From 88d7d7531086f64ffb8188169af80ab473ded2b1 Mon Sep 17 00:00:00 2001 From: xudaotutou <13435638964@163.com> Date: Tue, 21 Nov 2023 20:22:38 +0800 Subject: [PATCH] feat(costcenter): pinning table --- .../ui/src/components/icons/PortIcon.tsx | 33 + frontend/packages/ui/src/components/index.ts | 4 +- frontend/pnpm-lock.yaml | 63 +- .../costcenter/next-i18next.config.js | 3 +- frontend/providers/costcenter/next.config.js | 2 +- frontend/providers/costcenter/package.json | 1 + .../costcenter/public/locales/en/common.json | 1 + .../costcenter/public/locales/zh/common.json | 1 + .../src/components/billing/billingTable.tsx | 680 +++++++++++------- .../src/components/cost_overview/buget.tsx | 4 +- .../src/components/cost_overview/cost.tsx | 3 +- .../src/components/valuation/predictCard.tsx | 44 +- .../costcenter/src/constants/billing.ts | 16 + .../costcenter/src/constants/payment.ts | 3 +- .../costcenter/src/hooks/useBillingData.tsx | 12 +- .../costcenter/src/pages/api/billing/index.ts | 6 +- .../costcenter/src/pages/billing/index.tsx | 35 +- .../src/pages/cost_overview/index.tsx | 39 +- .../costcenter/src/pages/valuation/index.tsx | 15 +- .../providers/costcenter/src/types/billing.ts | 8 +- .../template/src/pages/deploy/index.tsx | 21 +- 21 files changed, 670 insertions(+), 324 deletions(-) create mode 100644 frontend/packages/ui/src/components/icons/PortIcon.tsx diff --git a/frontend/packages/ui/src/components/icons/PortIcon.tsx b/frontend/packages/ui/src/components/icons/PortIcon.tsx new file mode 100644 index 00000000000..b34b92b73db --- /dev/null +++ b/frontend/packages/ui/src/components/icons/PortIcon.tsx @@ -0,0 +1,33 @@ +import { createIcon } from '@chakra-ui/react'; + +const PortIcon = createIcon({ + displayName: 'PortIcon', + viewBox: '0 0 16 16', + path: ( + + + + + + + + ) +}); +export default PortIcon; diff --git a/frontend/packages/ui/src/components/index.ts b/frontend/packages/ui/src/components/index.ts index 6826be81084..b3bae79a837 100644 --- a/frontend/packages/ui/src/components/index.ts +++ b/frontend/packages/ui/src/components/index.ts @@ -37,6 +37,7 @@ import GithubIcon from './icons/GithubIcon'; import GoogleIcon from './icons/GoogleIcon'; import WechatIcon from './icons/WechatIcon'; import ListIcon from './icons/ListIcon'; +import PortIcon from './icons/PortIcon'; export { YamlCode, EditTabs, @@ -76,5 +77,6 @@ export { StorageIcon, UploadIcon, VisibityIcon, - WechatIcon + WechatIcon, + PortIcon }; diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 64cb3d86e11..712e0bcfc48 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -593,6 +593,9 @@ importers: '@tanstack/react-query-persist-client': specifier: ^4.35.5 version: 4.35.5(@tanstack/react-query@4.35.3) + '@tanstack/react-table': + specifier: ^8.10.7 + version: 8.10.7(react-dom@18.2.0)(react@18.2.0) axios: specifier: 1.2.1 version: 1.2.1 @@ -1283,7 +1286,7 @@ importers: version: 5.2.2 zustand: specifier: ^4.4.1 - version: 4.4.1(@types/react@18.2.37)(immer@9.0.21)(react@18.2.0) + version: 4.4.1(@types/react@18.2.37)(immer@10.0.2)(react@18.2.0) devDependencies: '@types/jest': specifier: ^29.5.5 @@ -4035,7 +4038,7 @@ packages: '@chakra-ui/react': 2.8.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.37)(framer-motion@10.16.4)(react-dom@18.2.0)(react@18.2.0) '@emotion/cache': 11.11.0 '@emotion/react': 11.11.1(@types/react@18.2.37)(react@18.2.0) - next: 13.5.6(@babel/core@7.23.3)(react-dom@18.2.0)(react@18.2.0) + next: 13.5.6(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 dev: false @@ -7321,6 +7324,23 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false + /@tanstack/react-table@8.10.7(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-bXhjA7xsTcsW8JPTTYlUg/FuBpn8MNjiEPhkNhIGCUR6iRQM2+WEco4OBpvDeVcR9SE+bmWLzdfiY7bCbCSVuA==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + dependencies: + '@tanstack/table-core': 8.10.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@tanstack/table-core@8.10.7: + resolution: {integrity: sha512-KQk5OMg5OH6rmbHZxuNROvdI+hKDIUxANaHlV+dPlNN7ED3qYQ/WkpY2qlXww1SIdeMlkIhpN/2L00rof0fXFw==} + engines: {node: '>=12'} + dev: false + /@testing-library/dom@9.3.3: resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==} engines: {node: '>=14'} @@ -14222,6 +14242,45 @@ packages: - babel-plugin-macros dev: false + /next@13.5.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==} + engines: {node: '>=16.14.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + sass: + optional: true + dependencies: + '@next/env': 13.5.6 + '@swc/helpers': 0.5.2 + busboy: 1.6.0 + caniuse-lite: 1.0.30001541 + postcss: 8.4.31 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.23.0)(react@18.2.0) + watchpack: 2.4.0 + optionalDependencies: + '@next/swc-darwin-arm64': 13.5.6 + '@next/swc-darwin-x64': 13.5.6 + '@next/swc-linux-arm64-gnu': 13.5.6 + '@next/swc-linux-arm64-musl': 13.5.6 + '@next/swc-linux-x64-gnu': 13.5.6 + '@next/swc-linux-x64-musl': 13.5.6 + '@next/swc-win32-arm64-msvc': 13.5.6 + '@next/swc-win32-ia32-msvc': 13.5.6 + '@next/swc-win32-x64-msvc': 13.5.6 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true diff --git a/frontend/providers/costcenter/next-i18next.config.js b/frontend/providers/costcenter/next-i18next.config.js index 8dca5135fe0..7a365a52cc5 100644 --- a/frontend/providers/costcenter/next-i18next.config.js +++ b/frontend/providers/costcenter/next-i18next.config.js @@ -8,5 +8,6 @@ module.exports = { defaultLocale: 'zh', locales: ['en', 'zh'], localeDetection: false - } + }, + reloadOnPrerender: process.env['NODE_ENV'] === 'development' }; diff --git a/frontend/providers/costcenter/next.config.js b/frontend/providers/costcenter/next.config.js index 16f95c24691..2401ff17367 100644 --- a/frontend/providers/costcenter/next.config.js +++ b/frontend/providers/costcenter/next.config.js @@ -13,7 +13,7 @@ const nextConfig = { i18n, reactStrictMode: false, output: 'standalone', - transpilePackages: ['echarts'], + transpilePackages: ['echarts', 'sealos@ui'], experimental: { // this includes files from the monorepo base two directories up outputFileTracingRoot: path.join(__dirname, '../../') diff --git a/frontend/providers/costcenter/package.json b/frontend/providers/costcenter/package.json index f23b3f66214..14596c915fb 100644 --- a/frontend/providers/costcenter/package.json +++ b/frontend/providers/costcenter/package.json @@ -25,6 +25,7 @@ "@tanstack/query-sync-storage-persister": "^4.35.3", "@tanstack/react-query": "^4.35.3", "@tanstack/react-query-persist-client": "^4.35.5", + "@tanstack/react-table": "^8.10.7", "axios": "1.2.1", "date-fns": "^2.30.0", "echarts": "^5.4.3", diff --git a/frontend/providers/costcenter/public/locales/en/common.json b/frontend/providers/costcenter/public/locales/en/common.json index 5bb02d4caf3..a663848d3b7 100644 --- a/frontend/providers/costcenter/public/locales/en/common.json +++ b/frontend/providers/costcenter/public/locales/en/common.json @@ -34,6 +34,7 @@ "Transaction Time": "Transaction Time", "Type": "Type", "CPU": "CPU", + "Port": "Port", "Memory": "Memory", "Network": "Network", "Storage": "Storage", diff --git a/frontend/providers/costcenter/public/locales/zh/common.json b/frontend/providers/costcenter/public/locales/zh/common.json index 211ce43b8b2..ed0aa12d0bd 100644 --- a/frontend/providers/costcenter/public/locales/zh/common.json +++ b/frontend/providers/costcenter/public/locales/zh/common.json @@ -35,6 +35,7 @@ "Transaction Time": "交易时间", "Type": "类型", "CPU": "CPU", + "Port": "端口", "Memory": "内存", "Storage": "存储卷", "Network": "网络", diff --git a/frontend/providers/costcenter/src/components/billing/billingTable.tsx b/frontend/providers/costcenter/src/components/billing/billingTable.tsx index a1e3a6725d4..d86a2590f53 100644 --- a/frontend/providers/costcenter/src/components/billing/billingTable.tsx +++ b/frontend/providers/costcenter/src/components/billing/billingTable.tsx @@ -1,4 +1,4 @@ -import { BasicTableHeaders, CATEGORY, TableHeaders } from '@/constants/billing'; +import { TableHeaderID } from '@/constants/billing'; import { BillingItem, BillingType } from '@/types/billing'; import lineDown from '@/assert/lineDown.svg'; import lineUp from '@/assert/lineUp.svg'; @@ -8,6 +8,7 @@ import { Img, Table, TableContainer, + TableContainerProps, Tbody, Td, Text, @@ -21,6 +22,342 @@ import { useTranslation } from 'next-i18next'; import useEnvStore from '@/stores/env'; import CurrencySymbol from '../CurrencySymbol'; import BillingDetails from './billingDetails'; +import { + useReactTable, + getCoreRowModel, + createColumnHelper, + flexRender, + HeaderContext, + CellContext, + Table as TTable +} from '@tanstack/react-table'; +import { useMemo } from 'react'; +import { enableGpu } from '@/service/enabled'; +export function CommonBillingTable({ + data, + ...styles +}: { data: BillingItem[] } & TableContainerProps) { + const { t } = useTranslation(); + const gpuEnabled = useEnvStore((state) => state.gpuEnabled); + 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) { + const original = props.row.original; + return ; + }; + return [ + columnHelper.accessor((row) => row.order_id, { + header: customTh(), + id: TableHeaderID.OrderNumber, + enablePinning: true, + cell(props) { + const item = props.row.original; + return ( + + + {item.order_id} + + + {item.namespace} + + + ); + } + }), + columnHelper.accessor((row) => row.time, { + header: customTh(), + id: TableHeaderID.TransactionTime, + enablePinning: true, + cell(props) { + return format(parseISO(props.cell.getValue()), 'MM-dd HH:mm'); + } + }), + columnHelper.accessor((row) => row.appType, { + header: customTh(), + id: TableHeaderID.APPType, + cell(props) { + return ( + + {props.cell.getValue()} + + ); + } + }), + columnHelper.accessor((row) => row.costs.cpu, { + id: TableHeaderID.CPU, + header: customTh(true), + cell: customCell() + }), + columnHelper.accessor((row) => row.costs.memory, { + id: TableHeaderID.Memory, + header: customTh(true), + cell: customCell() + }), + columnHelper.accessor((row) => row.costs.network, { + id: TableHeaderID.Network, + header: customTh(true), + cell: customCell() + }), + columnHelper.accessor((row) => row.costs.storage, { + id: TableHeaderID.Storage, + header: customTh(true), + cell: customCell() + }), + columnHelper.accessor((row) => row.costs.port, { + id: TableHeaderID.Port, + header: customTh(true), + cell: customCell() + }), + ...(gpuEnabled + ? [ + columnHelper.accessor((row) => row.costs.gpu, { + id: TableHeaderID.GPU, + header: customTh(true), + cell: customCell() + }) + ] + : []), + columnHelper.accessor((row) => row.amount, { + id: TableHeaderID.TotalAmount, + header: customTh(true), + cell: customCell(true) + }), + columnHelper.display({ + header: customTh(), + id: TableHeaderID.Handle, + enablePinning: true, + cell(props) { + const item = props.row.original; + return ( + + ); + } + }) + ]; + }, [enableGpu, t, currency]); + const table = useReactTable({ + data, + state: { + columnPinning: { + left: [TableHeaderID.OrderNumber], + right: [TableHeaderID.Handle] + } + }, + columns, + getCoreRowModel: getCoreRowModel() + }); + return ; +} +export function TransferBillingTable({ data }: { data: BillingItem[] }) { + 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 && } + + ); + }; + return [ + columnHelper.accessor((row) => row.order_id, { + header: customTh(), + id: TableHeaderID.OrderNumber, + enablePinning: true, + cell(props) { + const item = props.row.original; + return ( + + + {item.order_id} + + + {item.namespace} + + + ); + } + }), + columnHelper.accessor((row) => row.time, { + header: customTh(), + id: TableHeaderID.TransactionTime, + enablePinning: true, + cell(props) { + return format(parseISO(props.cell.getValue()), 'MM-dd HH:mm'); + } + }), + columnHelper.accessor((row) => row.appType, { + id: TableHeaderID.APPType, + header: customTh(true), + cell(props) { + const item = props.row.original; + return ( + + + + {item.type === BillingType.RECEIVE ? t('Recipient') : t('Transfer')} + + + ); + } + }), + columnHelper.accessor((row) => row.amount, { + header: customTh(), + id: TableHeaderID.TotalAmount, + cell(props) { + const original = props.row.original; + return ; + } + }), + columnHelper.accessor((row) => row.namespace, { + id: TableHeaderID.Namespace, + header: customTh(), + cell(props) { + const item = props.cell.getValue(); + return {item}; + } + }) + ]; + }, [enableGpu, t, currency]); + + const table = useReactTable({ + data, + state: { + columnPinning: { + left: [TableHeaderID.OrderNumber], + right: [TableHeaderID.Namespace] + } + }, + columns, + getCoreRowModel: getCoreRowModel() + }); + return ; +} + +export function BillingDetailsTable({ + data, + ...styles +}: { data: BillingItem[] } & TableContainerProps) { + const { t } = useTranslation(); + const gpuEnabled = useEnvStore((state) => state.gpuEnabled); + 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) { + const original = props.row.original; + return ; + }; + return [ + columnHelper.accessor((row) => row.name, { + header: customTh(), + id: TableHeaderID.APPName, + enablePinning: true + }), + columnHelper.accessor((row) => row.costs.cpu, { + id: TableHeaderID.CPU, + header: customTh(true), + cell: customCell() + }), + columnHelper.accessor((row) => row.costs.memory, { + id: TableHeaderID.Memory, + header: customTh(true), + cell: customCell() + }), + columnHelper.accessor((row) => row.costs.network, { + id: TableHeaderID.Network, + header: customTh(true), + cell: customCell() + }), + columnHelper.accessor((row) => row.costs.storage, { + id: TableHeaderID.Storage, + header: customTh(true), + cell: customCell() + }), + columnHelper.accessor((row) => row.costs.port, { + id: TableHeaderID.Port, + header: customTh(true), + cell: customCell() + }), + ...(gpuEnabled + ? [ + columnHelper.accessor((row) => row.costs.gpu, { + id: TableHeaderID.GPU, + header: customTh(true), + cell: customCell() + }) + ] + : []), + columnHelper.accessor((row) => row.amount, { + id: TableHeaderID.TotalAmount, + header: customTh(true), + cell: customCell(true) + }) + ]; + }, [enableGpu, t, currency]); + const table = useReactTable({ + data, + state: { + columnPinning: { + left: [TableHeaderID.APPName], + right: [TableHeaderID.TotalAmount] + } + }, + columns, + getCoreRowModel: getCoreRowModel() + }); + return ; +} const Amount = ({ type, amount, @@ -38,254 +375,115 @@ const Amount = ({ return +{formatMoney(amount)}; else return -; }; -export function CommonBillingTable({ data }: { data: BillingItem[] }) { - const { t } = useTranslation(); - const gpuEnabled = useEnvStore((state) => state.gpuEnabled); - const currency = useEnvStore((s) => s.currency); +const BaseTable = ({ + table, + ...styles +}: { table: TTable } & TableContainerProps) => { + console.log(table.getColumn(TableHeaderID.TotalAmount)?.getPinnedIndex()); return ( - - + +
- - {[ - ...BasicTableHeaders, - ...TableHeaders, - ...(gpuEnabled ? ['Gpu'] : []), - 'Total Amount', - 'Handle' - ].map((item) => ( - - ))} - - - - {data - ?.filter((item) => [BillingType.CONSUME, BillingType.RECHARGE].includes(item.type)) - .map((item) => { - return ( - - - - - - - - - {gpuEnabled && ( - - )} - - - - ); - })} - -
- - {t(item)} - {['CPU', 'Gpu', 'Memory', 'Storage', 'Network', 'Total Amount'].includes( - item - ) && } - -
- - - {item.order_id} - - - {item.namespace} - - - {format(parseISO(item.time), 'MM-dd HH:mm')} - - {item.appType} - - - - - - - - - - - - - - - -
-
- ); -} -export function TransferBillingTable({ data }: { data: BillingItem[] }) { - const { t } = useTranslation(); - const currency = useEnvStore((s) => s.currency); - return ( - - - - - {[...BasicTableHeaders, 'Total Amount', 'Namespace'].map((item) => ( - - ))} - - - - {data - ?.filter((item) => [BillingType.RECEIVE, BillingType.TRANSFER].includes(item.type)) - .map((item) => { - return ( - - - - - - - - ); - })} - -
- - {t(item)} - {['Total Amount'].includes(item) && } - -
{item.order_id}{format(parseISO(item.time), 'MM-dd HH:mm')} - - - - - {item.type === BillingType.RECEIVE ? t('Recipient') : t('Transfer')} - - - - - - {{item.namespace}}
-
- ); -} - -export function BillingDetailsTable({ data }: { data: BillingItem[] }) { - const { t } = useTranslation(); - const gpuEnabled = useEnvStore((state) => state.gpuEnabled); - const currency = useEnvStore((s) => s.currency); - return ( - - - - - {['APP Name', ...TableHeaders, ...(gpuEnabled ? ['Gpu'] : []), 'Total Amount'].map( - (item) => ( - - ) - )} - + {table.getHeaderGroups().map((headers) => { + return ( + + {headers.headers.map((header) => { + const pinState = header.column.getIsPinned(); + return ( + + ); + })} + + ); + })} - - {data.map((item) => { + + {table.getRowModel().rows.map((item) => { return ( - - - - - - - {gpuEnabled && ( - - )} - + + {item.getAllCells().map((cell) => { + const pinState = cell.column.getIsPinned(); + return ( + + ); + })} ); })} + {}
- - {t(item)} - {['CPU', 'Gpu', 'Memory', 'Storage', 'Network', 'Total Amount'].includes( - item - ) && } - -
+ {flexRender(header.column.columnDef.header, header.getContext())} +
{item.name} - - - - - - - - - - - -
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
); -} +}; + +BaseTable.displayName = 'BaseTable'; diff --git a/frontend/providers/costcenter/src/components/cost_overview/buget.tsx b/frontend/providers/costcenter/src/components/cost_overview/buget.tsx index 387091e8816..adf30d72cd5 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/buget.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/buget.tsx @@ -3,7 +3,7 @@ import { Card, CardBody } from '@chakra-ui/react'; import down_icon from '@/assert/ic_round-trending-down.svg'; import up_icon from '@/assert/ic_round-trending-up.svg'; import { useMemo } from 'react'; -import { formatMoney } from '@/utils/format'; +import { displayMoney, formatMoney } from '@/utils/format'; import { useTranslation } from 'next-i18next'; import useBillingData from '@/hooks/useBillingData'; import CurrencySymbol from '../CurrencySymbol'; @@ -56,7 +56,7 @@ export function Buget() { - {v.value} + {displayMoney(v.value)} diff --git a/frontend/providers/costcenter/src/components/cost_overview/cost.tsx b/frontend/providers/costcenter/src/components/cost_overview/cost.tsx index 5ecb752a861..fd2b4d1fd04 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/cost.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/cost.tsx @@ -16,7 +16,8 @@ export const Cost = memo(function Cost() { memory: _deduction?.memory || 0, storage: _deduction?.storage || 0, network: _deduction?.network || 0, - gpu: _deduction?.gpu || 0 + gpu: _deduction?.gpu || 0, + port: _deduction?.port || 0 }; return ( diff --git a/frontend/providers/costcenter/src/components/valuation/predictCard.tsx b/frontend/providers/costcenter/src/components/valuation/predictCard.tsx index 43d86ba3cec..81a028b830c 100644 --- a/frontend/providers/costcenter/src/components/valuation/predictCard.tsx +++ b/frontend/providers/costcenter/src/components/valuation/predictCard.tsx @@ -1,29 +1,55 @@ import useBillingStore from '@/stores/billing'; import useEnvStore from '@/stores/env'; -import { displayMoney } from '@/utils/format'; -import { Box, Flex, Img, Stack, Text } from '@chakra-ui/react'; +import { displayMoney, formatMoney } from '@/utils/format'; +import { Box, Flex, Stack, Text, filter } from '@chakra-ui/react'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import CurrencySymbol from '../CurrencySymbol'; -import { valuationMap } from '@/constants/payment'; +import { END_TIME, valuationMap } from '@/constants/payment'; +import useBillingData from '@/hooks/useBillingData'; +import { BillingType, Costs } from '@/types'; +import { isSameDay, isSameHour, parseISO } from 'date-fns'; export default function PredictCard() { const { t } = useTranslation(); - const state = useBillingStore(); + const { data } = useBillingData({ type: BillingType.CONSUME, endTime: END_TIME }); + const _state = useMemo(() => { + const items = data?.data?.status.item || []; + if (items.length > 0) { + const latest = items[0]; + const time = parseISO(latest.time); + const now = new Date(); + if (isSameDay(time, now) && isSameHour(time, now)) + return { + ...latest.costs, + total: latest.amount + }; + } + return { + cpu: 0, + memory: 0, + storage: 0, + network: 0, + port: 0, + total: 0 + }; + }, [data?.data?.status.item]); const currency = useEnvStore((s) => s.currency); const gpuEnabled = useEnvStore((state) => state.gpuEnabled); const leastCost = useMemo(() => { + const state = Object.fromEntries(Object.entries(_state).map(([k, v]) => [k, formatMoney(v)])); const origin = [ { name: 'CPU', cost: state.cpu }, { name: 'Memory', cost: state.memory }, { name: 'Storage', cost: state.storage }, - { name: 'Network', cost: state.network } + { name: 'Network', cost: state.network }, + { name: 'Port', cost: state.port } ]; if (!gpuEnabled) { - origin.push({ name: 'Total Amount', cost: state.cpu + state.memory + state.storage }); + origin.push({ name: 'Total Amount', cost: state.total }); } else { origin.push( - { name: 'GPU', cost: state.gpu }, - { name: 'Total Amount', cost: state.cpu + state.memory + state.storage + state.gpu } + { name: 'GPU', cost: state.gpu ?? 0 }, + { name: 'Total Amount', cost: state.total } ); } return origin.map((item) => ({ @@ -31,7 +57,7 @@ export default function PredictCard() { cost: displayMoney(item.cost * 30 * 24), color: valuationMap.get(item.name.toLocaleLowerCase())?.bg || 'black' })); - }, [state.cpu, state.memory, state.storage, gpuEnabled]); + }, [_state, gpuEnabled]); return ( {leastCost.map((item) => ( diff --git a/frontend/providers/costcenter/src/constants/billing.ts b/frontend/providers/costcenter/src/constants/billing.ts index f707cdab111..3d0b0245f39 100644 --- a/frontend/providers/costcenter/src/constants/billing.ts +++ b/frontend/providers/costcenter/src/constants/billing.ts @@ -19,3 +19,19 @@ export const LIST_TYPE: { title: string; value: BillingType }[] = [ { title: 'Recipient', value: BillingType.RECEIVE }, { title: 'Transfer', value: BillingType.TRANSFER } ]; +export enum TableHeaderID { + 'APPName' = 'APP Name', + 'OrderNumber' = 'Order Number', + 'TransactionTime' = 'Transaction Time', + 'APPType' = 'APP Type', + 'CPU' = 'CPU', + 'GPU' = 'GPU', + 'Port' = 'Port', + 'TrueAmount' = 'True Amount', + 'Memory' = 'Memory', + 'Storage' = 'Storage', + 'Network' = 'Network', + 'TotalAmount' = 'Total Amount', + 'Handle' = 'Handle', + 'Namespace' = 'Namespace' +} diff --git a/frontend/providers/costcenter/src/constants/payment.ts b/frontend/providers/costcenter/src/constants/payment.ts index a8ccf4b1658..c2f95b66141 100644 --- a/frontend/providers/costcenter/src/constants/payment.ts +++ b/frontend/providers/costcenter/src/constants/payment.ts @@ -92,5 +92,6 @@ export const valuationMap = new Map([ ['memory', { unit: 'GB', scale: 1024, bg: '#36ADEF', idx: 1 }], ['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 }] + ['network', { unit: 'M', scale: 1, bg: '#F182AA', idx: 4 }], + ['services.nodeports', { unit: '', scale: 1, bg: '#F182AA', idx: 4 }] ]); diff --git a/frontend/providers/costcenter/src/hooks/useBillingData.tsx b/frontend/providers/costcenter/src/hooks/useBillingData.tsx index 708d473f3c6..f713c88f26e 100644 --- a/frontend/providers/costcenter/src/hooks/useBillingData.tsx +++ b/frontend/providers/costcenter/src/hooks/useBillingData.tsx @@ -5,21 +5,25 @@ import { BillingSpec, BillingData } from '@/types/billing'; import { useQuery } from '@tanstack/react-query'; import { differenceInDays, formatISO } from 'date-fns'; -export default function useBillingData(props?: { type: -1 | 0 | 1 | 2 | 3 }) { +export default function useBillingData(props?: { + type: -1 | 0 | 1 | 2 | 3; + endTime?: Date; + startTime?: Date; +}) { const startTime = useOverviewStore((state) => state.startTime); const endTime = useOverviewStore((state) => state.endTime); return useQuery({ queryKey: ['billing', { startTime, endTime }], queryFn: () => { - const start = startTime; - const end = endTime; + const start = props?.startTime ?? startTime; + const end = props?.endTime ?? endTime; const delta = differenceInDays(end, start); const spec: BillingSpec = { startTime: formatISO(start, { representation: 'complete' }), endTime: formatISO(end, { representation: 'complete' }), page: 1, pageSize: (delta + 1) * 48, - type: props?.type || -1, + type: props?.type ?? -1, orderID: '' }; return request, { spec: BillingSpec }>('/api/billing', { diff --git a/frontend/providers/costcenter/src/pages/api/billing/index.ts b/frontend/providers/costcenter/src/pages/api/billing/index.ts index 654c22cdc99..9d694e50644 100644 --- a/frontend/providers/costcenter/src/pages/api/billing/index.ts +++ b/frontend/providers/costcenter/src/pages/api/billing/index.ts @@ -8,12 +8,13 @@ import crypto from 'crypto'; import type { BillingData, BillingItem, BillingSpec, Costs, RawCosts } from '@/types/billing'; const convertGpu = (_deduction?: RawCosts) => _deduction - ? Object.entries(_deduction).reduce( + ? (Object.entries(_deduction) as [keyof RawCosts, number][]).reduce( (pre, cur) => { if (cur[0] === 'cpu') pre.cpu = cur[1]; else if (cur[0] === 'memory') pre.memory = cur[1]; else if (cur[0] === 'storage') pre.storage = cur[1]; else if (cur[0] === 'network') pre.network = cur[1]; + else if (cur[0] === 'services.nodeports') pre.port = cur[1]; else if (cur[0].startsWith('gpu-')) { typeof pre.gpu === 'number' && (pre.gpu += cur[1]); } @@ -24,6 +25,7 @@ const convertGpu = (_deduction?: RawCosts) => memory: 0, storage: 0, gpu: 0, + port: 0, network: 0 } ) @@ -31,6 +33,7 @@ const convertGpu = (_deduction?: RawCosts) => cpu: 0, memory: 0, storage: 0, + port: 0, network: 0, gpu: 0 }; @@ -74,6 +77,7 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse const crd = (await GetCRD(kc, meta, name)) as { body: BillingData }; const body = crd?.body; if (!body || !body.status) throw new Error('get billing error'); + console.log(body.status.item); const item = body.status?.item?.map((v) => ({ ...v, diff --git a/frontend/providers/costcenter/src/pages/billing/index.tsx b/frontend/providers/costcenter/src/pages/billing/index.tsx index 7342b8fb806..b0b476fd5c9 100644 --- a/frontend/providers/costcenter/src/pages/billing/index.tsx +++ b/frontend/providers/costcenter/src/pages/billing/index.tsx @@ -117,7 +117,7 @@ function InOutTabPanel({ namespace }: { namespace: string }) { const { t } = useTranslation(); const tableResult = data?.data?.status?.item || []; return ( - + @@ -146,13 +146,14 @@ function InOutTabPanel({ namespace }: { namespace: string }) { {isSuccess && tableResult.length > 0 ? ( <> - - - [BillingType.CONSUME, BillingType.RECHARGE].includes(x.type) - )} - /> - + + [BillingType.CONSUME, BillingType.RECHARGE].includes(x.type) + )} + flex={'auto'} + overflow={'auto'} + overflowY={'auto'} + /> + @@ -248,13 +249,11 @@ function TransferTabPanel({ namespace }: { namespace: string }) { {isSuccess && tableResult.length > 0 ? ( <> - - - [BillingType.RECEIVE, BillingType.TRANSFER].includes(x.type) - )} - /> - + + [BillingType.RECEIVE, BillingType.TRANSFER].includes(x.type) + )} + /> {t('SideBar.BillingDetails')} - + - + diff --git a/frontend/providers/costcenter/src/pages/cost_overview/index.tsx b/frontend/providers/costcenter/src/pages/cost_overview/index.tsx index fc9c2aa35b9..dcc196a3e30 100644 --- a/frontend/providers/costcenter/src/pages/cost_overview/index.tsx +++ b/frontend/providers/costcenter/src/pages/cost_overview/index.tsx @@ -12,8 +12,6 @@ import { Trend } from '@/components/cost_overview/trend'; import { getCookie } from '@/utils/cookieUtils'; import useBillingData from '@/hooks/useBillingData'; import NotFound from '@/components/notFound'; -import useBillingStore from '@/stores/billing'; -import { isSameDay, isSameHour, parseISO } from 'date-fns'; import { useRouter } from 'next/router'; import useOverviewStore from '@/stores/overview'; import { CommonBillingTable } from '@/components/billing/billingTable'; @@ -23,11 +21,6 @@ export const RechargeContext = createContext<{ rechargeRef: MutableRefObject state.updateCpu); - const updateMemory = useBillingStore((state) => state.updateMemory); - const updateStorage = useBillingStore((state) => state.updateStorage); - const updateNetwork = useBillingStore((state) => state.updateNetwork); - const updateGpu = useBillingStore((state) => state.updateGpu); const cookie = getCookie('NEXT_LOCALE'); useEffect(() => { i18n.changeLanguage(cookie); @@ -64,21 +57,9 @@ function CostOverview() { }, []); const { NotEnoughModal } = useNotEnough(); const { data, isInitialLoading } = useBillingData(); - const billingItems = data?.data?.status.item.filter((_v, i) => i < 3) || []; const costBillingItems = data?.data?.status.item.filter((v) => v.type === 0) || []; + const billingItems = costBillingItems.filter((_v, i) => i < 3) || []; const totast = useToast(); - useEffect(() => { - if (costBillingItems.length === 0) return; - const time = parseISO(costBillingItems[0].time); - const now = new Date(); - if (!isSameDay(time, now) || !isSameHour(time, now)) return; - const item = costBillingItems[0].costs; - updateCPU(item?.cpu || 0); - updateMemory(item?.memory || 0); - updateStorage(item?.storage || 0); - updateNetwork(item?.network || 0); - updateGpu(item?.gpu || 0); - }, [costBillingItems, updateCPU, updateMemory, updateStorage]); const rechargeRef = useRef(); return ( @@ -102,7 +83,7 @@ function CostOverview() { - + @@ -113,18 +94,16 @@ function CostOverview() { - + {t('Recent Transactions')} - - - {(isInitialLoading || billingItems.length === 0) && ( - - - - )} - + + {(isInitialLoading || billingItems.length === 0) && ( + + + + )} diff --git a/frontend/providers/costcenter/src/pages/valuation/index.tsx b/frontend/providers/costcenter/src/pages/valuation/index.tsx index ce8676506ed..255b45bee78 100644 --- a/frontend/providers/costcenter/src/pages/valuation/index.tsx +++ b/frontend/providers/costcenter/src/pages/valuation/index.tsx @@ -40,6 +40,7 @@ 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'; type CardItem = { title: string; price: number[]; @@ -143,14 +144,18 @@ function Valuation() { const props = valuationMap.get(x.resourceType); if (!props) return []; let icon; + let title = x.resourceType; if (x.resourceType === 'cpu') icon = CpuIcon; else if (x.resourceType === 'memory') icon = MemoryIcon; else if (x.resourceType === 'network') icon = NetworkIcon; else if (x.resourceType === 'storage') icon = StorageIcon; - else return []; + else if (x.resourceType === 'services.nodeports') { + icon = PortIcon; + title = 'Port'; + } else return []; return [ { - title: x.resourceType, + title, price: [1, 24, 168, 720, 8760].map( (v) => Math.floor(v * x.price * (props.scale || 1)) / 1000000 ), @@ -221,7 +226,11 @@ function Valuation() { {t(x.title)} - {x.unit + (x.title !== 'network' ? `/${t(CYCLE[cycleIdx])}` : '')} + + {[x.unit, x.title !== 'network' ? `${t(CYCLE[cycleIdx])}` : ''] + .filter((v) => v.trim() !== '') + .join('/')} + {x.title === 'network' ? x.price[0] : x.price[cycleIdx]} ))} diff --git a/frontend/providers/costcenter/src/types/billing.ts b/frontend/providers/costcenter/src/types/billing.ts index a621873fd55..d9ac94fb74c 100644 --- a/frontend/providers/costcenter/src/types/billing.ts +++ b/frontend/providers/costcenter/src/types/billing.ts @@ -18,12 +18,16 @@ export type BillingSpec = | { orderID: string; //如果给定orderId,则查找该id的值,该值为唯一值,因此当orderId给定时忽略其他查找限定值 }; -export type RawCosts = Record<'network' | 'cpu' | 'memory' | 'storage' | `gpu-${string}`, number>; +export type RawCosts = Record< + 'network' | 'cpu' | 'memory' | 'storage' | `gpu-${string}` | 'services.nodeports', + number +>; export type Costs = { cpu: number; memory: number; storage: number; network: number; + port: number; gpu?: number; }; export type BillingItem = { @@ -47,7 +51,7 @@ export type BillingData = { spec: BillingSpec; status: { deductionAmount: T; - item: BillingItem[]; + item: BillingItem[]; pageLength: number; totalCount: number; rechargeAmount: number; diff --git a/frontend/providers/template/src/pages/deploy/index.tsx b/frontend/providers/template/src/pages/deploy/index.tsx index 21f2389cff7..a9c994ac3db 100644 --- a/frontend/providers/template/src/pages/deploy/index.tsx +++ b/frontend/providers/template/src/pages/deploy/index.tsx @@ -256,7 +256,8 @@ export default function EditApp({ appName }: { appName?: string }) { overflow={'auto'} position={'relative'} borderRadius={'12px'} - background={'linear-gradient(180deg, #FFF 0%, rgba(255, 255, 255, 0.70) 100%)'}> + background={'linear-gradient(180deg, #FFF 0%, rgba(255, 255, 255, 0.70) 100%)'} + > + backdropBlur={'100px'} + > + cursor={'pointer'} + > + }} + > router.push('/')}> + onClick={() => router.push('/')} + > router.push('/')}> @@ -303,7 +308,8 @@ export default function EditApp({ appName }: { appName?: string }) { + color={router.pathname === '/deploy' ? '#262A32' : '#7B838B'} + > {data?.templateYaml?.metadata?.name} @@ -314,7 +320,8 @@ export default function EditApp({ appName }: { appName?: string }) { flexDirection={'column'} width={'100%'} flexGrow={1} - backgroundColor={'rgba(255, 255, 255, 0.90)'}> + backgroundColor={'rgba(255, 255, 255, 0.90)'} + >