From ae86b84dd95a39d51391eb470a340776fd97a195 Mon Sep 17 00:00:00 2001 From: devinxl Date: Fri, 31 May 2024 11:47:47 +0800 Subject: [PATCH 1/4] feat(dcellar-web-ui): add the download quota usage chart --- .../charts/LineChart/useLineChartOptions.ts | 30 +-- apps/dcellar-web-ui/src/facade/dashboard.ts | 48 +++++ apps/dcellar-web-ui/src/facade/explorer.ts | 28 --- .../dashboard/components/BucketQuotaUsage.tsx | 173 ++++++++++++++++++ ...torageChart.tsx => BucketStorageUsage.tsx} | 68 ++++--- .../components/BucketUsageStatistics.tsx | 36 ++++ .../{FilterBuckets.tsx => BucketsFilter.tsx} | 59 +++--- .../src/modules/dashboard/index.tsx | 9 +- .../src/store/slices/dashboard.ts | 115 +++++++++--- .../src/utils/dashboard/index.ts | 6 + 10 files changed, 448 insertions(+), 124 deletions(-) create mode 100644 apps/dcellar-web-ui/src/facade/dashboard.ts delete mode 100644 apps/dcellar-web-ui/src/facade/explorer.ts create mode 100644 apps/dcellar-web-ui/src/modules/dashboard/components/BucketQuotaUsage.tsx rename apps/dcellar-web-ui/src/modules/dashboard/components/{BucketStorageChart.tsx => BucketStorageUsage.tsx} (67%) create mode 100644 apps/dcellar-web-ui/src/modules/dashboard/components/BucketUsageStatistics.tsx rename apps/dcellar-web-ui/src/modules/dashboard/components/{FilterBuckets.tsx => BucketsFilter.tsx} (74%) diff --git a/apps/dcellar-web-ui/src/components/charts/LineChart/useLineChartOptions.ts b/apps/dcellar-web-ui/src/components/charts/LineChart/useLineChartOptions.ts index 9537de41..6cbb06fc 100644 --- a/apps/dcellar-web-ui/src/components/charts/LineChart/useLineChartOptions.ts +++ b/apps/dcellar-web-ui/src/components/charts/LineChart/useLineChartOptions.ts @@ -47,9 +47,27 @@ export function useLineChartOptions(options: any, noData: boolean) { `; }, }, + grid: { + left: 20, + right: 20, + top: 20, + bottom: 0, + containLabel: true, + }, + legend: { + icon: 'circle', + itemHeight: 8, + itemWidth: 8, + itemGap: 16, + right: 0, + textStyle: { + fontWeight: 400, + }, + }, xAxis: { type: 'category', boundaryGap: false, + contianerLabel: true, axisLabel: { marginTop: 4, fontWeight: 500, @@ -81,11 +99,6 @@ export function useLineChartOptions(options: any, noData: boolean) { show: true, type: 'value', scale: true, - spiltLine: { - lineStyle: { - color: 'red', - }, - }, axisTick: { show: true, alignWithLabel: true, @@ -111,13 +124,6 @@ export function useLineChartOptions(options: any, noData: boolean) { fontFamily: 'Inter', }, }, - grid: { - left: 4, - right: 20, - top: 6, - bottom: 0, - containLabel: true, - }, series: [ { type: 'line', diff --git a/apps/dcellar-web-ui/src/facade/dashboard.ts b/apps/dcellar-web-ui/src/facade/dashboard.ts new file mode 100644 index 00000000..1a04d016 --- /dev/null +++ b/apps/dcellar-web-ui/src/facade/dashboard.ts @@ -0,0 +1,48 @@ +import { get } from '@/base/http'; +import { ErrorResponse, commonFault } from './error'; + +export type GetBucketDailyUsageByOwnerParams = { + page: number; + per_page: number; + owner: string; +}; + +export type BucketDailyStorageUsage = { + BucketNumID: string; + BucketID: string; + BucketName: string; + Owner: string; + TotalTxCount: number; + DailyTxCount: number; + DailyTxCountList: number[]; + DailyTotalChargedStorageSize: string[]; +}; + +export type BucketDailyQuotaUsage = { + BucketID: string; + BucketName: string; + MonthlyQuotaSize: string; + MonthlyQuotaConsumedSize: number; + Date: number; +}; + +// key is bucketId +export type BucketDailyQuotaUsageResponse = Record; + +export const getBucketDailyStorageUsage = ( + params: GetBucketDailyUsageByOwnerParams, +): Promise<[BucketDailyStorageUsage[], null] | ErrorResponse> => { + const url = `/api/chart/daily_bucket/list`; + return get({ url, data: params }).then((res) => { + return [res.result, null]; + }, commonFault); +}; + +export const getBucketDailyQuotaUsage = ( + params: GetBucketDailyUsageByOwnerParams, +): Promise<[BucketDailyQuotaUsageResponse, null] | ErrorResponse> => { + const url = `/api/chart/daily_bucket_quota/list`; + return get({ url, data: params }).then((res) => { + return [res.result, null]; + }, commonFault); +}; diff --git a/apps/dcellar-web-ui/src/facade/explorer.ts b/apps/dcellar-web-ui/src/facade/explorer.ts deleted file mode 100644 index 0cc96eee..00000000 --- a/apps/dcellar-web-ui/src/facade/explorer.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { get } from '@/base/http'; -import { ErrorResponse, commonFault } from './error'; - -export type GetDailyBucketListByOwnerParams = { - page: number; - per_page: number; - owner: string; -}; - -export type DailyBucketStorage = { - BucketNumID: string; - BucketID: string; - BucketName: string; - Owner: string; - TotalTxCount: number; - DailyTxCount: number; - DailyTxCountList: number[]; - DailyTotalChargedStorageSize: string[]; -}; - -export const getDailyBucketStorageListByOwner = ( - params: GetDailyBucketListByOwnerParams, -): Promise<[DailyBucketStorage[], null] | ErrorResponse> => { - const url = `/api/chart/daily_bucket/list`; - return get({ url, data: params }).then((res) => { - return [res.result, null]; - }, commonFault); -}; diff --git a/apps/dcellar-web-ui/src/modules/dashboard/components/BucketQuotaUsage.tsx b/apps/dcellar-web-ui/src/modules/dashboard/components/BucketQuotaUsage.tsx new file mode 100644 index 00000000..f66972e2 --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/dashboard/components/BucketQuotaUsage.tsx @@ -0,0 +1,173 @@ +import { LineChart } from '@/components/charts/LineChart'; +import { Loading } from '@/components/common/Loading'; +import { FilterContainer } from '@/modules/accounts/components/Common'; +import { useAppDispatch, useAppSelector } from '@/store'; +import { setBucketDailyQuotaFilter } from '@/store/slices/dashboard'; +import { formatChartTime, mergeArr } from '@/utils/dashboard'; +import { formatBytes } from '@/utils/formatter'; +import { getMillisecond, getUtcDayjs } from '@/utils/time'; +import { Box, Flex } from '@node-real/uikit'; +import { isEmpty } from 'lodash-es'; +import { useMemo } from 'react'; +import { LABEL_STYLES, VALUE_STYLES } from '../constants'; +import { + selectBucketDailyQuotaUsage, + selectFilterQuotaUsageBuckets, +} from '@/store/slices/dashboard'; +import { BN } from '@/utils/math'; +import { BucketsFilter } from './BucketsFilter'; + +export const BucketQuotaUsage = () => { + const dispatch = useAppDispatch(); + const loginAccount = useAppSelector((root) => root.persist.loginAccount); + const bucketDailyQuotaUsage = useAppSelector(selectBucketDailyQuotaUsage()); + const filteredBuckets = useAppSelector(selectFilterQuotaUsageBuckets()); + const isLoading = bucketDailyQuotaUsage === undefined; + const dayjs = getUtcDayjs(); + const noData = !isLoading && isEmpty(bucketDailyQuotaUsage); + const bucketNames = Object.keys(bucketDailyQuotaUsage); + + const onBucketFiltered = (bucketNames: string[]) => { + dispatch(setBucketDailyQuotaFilter({ loginAccount, bucketNames })); + }; + + const groupDataByTime = useMemo(() => { + const data = bucketDailyQuotaUsage || {}; + const filterData = isEmpty(filteredBuckets) + ? data + : filteredBuckets.map((item) => data[item]).filter((item) => item !== undefined); + const groupDataByTime: Record< + string, + { MonthlyQuotaSize: string; MonthlyQuotaConsumedSize: string } + > = {}; + Object.values(filterData).forEach((quotaUsages) => { + quotaUsages.forEach((item) => { + if (!groupDataByTime[item.Date]) { + groupDataByTime[item.Date] = { + MonthlyQuotaSize: String(item.MonthlyQuotaSize), + MonthlyQuotaConsumedSize: String(item.MonthlyQuotaConsumedSize), + }; + } + if (groupDataByTime[item.Date]) { + groupDataByTime[item.Date] = { + MonthlyQuotaSize: BN(groupDataByTime[item.Date].MonthlyQuotaSize) + .plus(item.MonthlyQuotaSize) + .toString(), + MonthlyQuotaConsumedSize: BN(groupDataByTime[item.Date].MonthlyQuotaConsumedSize) + .plus(BN(item.MonthlyQuotaConsumedSize)) + .toString(), + }; + } + }); + }); + return groupDataByTime; + }, [bucketDailyQuotaUsage, filteredBuckets]); + + const lineOptions = useMemo(() => { + const lineData = Object.entries(groupDataByTime).map(([time, quotaData]) => ({ + time: getMillisecond(+time), + totalQuota: quotaData.MonthlyQuotaSize, + quotaUsage: quotaData.MonthlyQuotaConsumedSize, + formatTime: dayjs(getMillisecond(+time)).format('DD, MMM'), + formatSize: formatBytes(quotaData.MonthlyQuotaSize), + })); + const xData = lineData.map((item) => item.formatTime); + const yQuotaUsage = lineData.map((item) => item.quotaUsage); + const yTotalQuota = lineData.map((item) => item.totalQuota); + + return { + tooltip: { + trigger: 'axis', + content: (params: any) => { + const { data: quotaUsage } = + params.find((item: any) => item.seriesName === 'Quota Usage') || {}; + const { data: totalQuota } = + params.find((item: any) => item.seriesName === 'Total Quota') || {}; + const curData = lineData[params[0].dataIndex] || {}; + + const TotalQuotaFragment = + totalQuota !== undefined + ? `

Total Quota: ${formatBytes(totalQuota)}

` + : ''; + const QuotaUsageFragment = + quotaUsage !== undefined + ? `

Quota Usage: ${formatBytes(quotaUsage)}

` + : ''; + return ` +

${formatChartTime(curData.time)}

+ ${TotalQuotaFragment} + ${QuotaUsageFragment} + `; + }, + }, + legend: { + icon: 'circle', + itemHeight: 8, + itemWidth: 8, + itemGap: 16, + right: 12, + textStyle: { fontWeight: 400 }, + data: ['Total Quota', 'Quota Usage'], + }, + xAxis: { + data: xData, + }, + yAxis: { + axisLabel: { + formatter: (value: string, index: number) => { + return formatBytes(value); + }, + }, + }, + series: [ + { + symbol: 'circle', + symbolSize: 5, + lineStyle: { color: '#00BA34' }, + itemStyle: { + color: '#00BA34', + opacity: 1, + }, + smooth: false, + name: 'Quota Usage', + type: 'line', + stack: 'Quota Usage', + data: yQuotaUsage, + }, + { + symbol: 'circle', + symbolSize: 5, + lineStyle: { color: '#EE7C11' }, + itemStyle: { + color: '#EE7C11', + }, + emphasis: { itemStyle: { opacity: 1 } }, + animationDuration: 600, + smooth: false, + name: 'Total Quota', + type: 'line', + stack: 'Total Quota', + data: yTotalQuota, + }, + ], + }; + }, [groupDataByTime, dayjs]); + + return ( + + + + + + + {isLoading && } + {!isLoading && } + + + + ); +}; diff --git a/apps/dcellar-web-ui/src/modules/dashboard/components/BucketStorageChart.tsx b/apps/dcellar-web-ui/src/modules/dashboard/components/BucketStorageUsage.tsx similarity index 67% rename from apps/dcellar-web-ui/src/modules/dashboard/components/BucketStorageChart.tsx rename to apps/dcellar-web-ui/src/modules/dashboard/components/BucketStorageUsage.tsx index ff1ffad5..b09ad5f9 100644 --- a/apps/dcellar-web-ui/src/modules/dashboard/components/BucketStorageChart.tsx +++ b/apps/dcellar-web-ui/src/modules/dashboard/components/BucketStorageUsage.tsx @@ -1,9 +1,12 @@ import { LineChart } from '@/components/charts/LineChart'; -import { DCButton } from '@/components/common/DCButton'; import { Loading } from '@/components/common/Loading'; import { FilterContainer } from '@/modules/accounts/components/Common'; -import { useAppSelector } from '@/store'; -import { selectFilterBuckets } from '@/store/slices/dashboard'; +import { useAppDispatch, useAppSelector } from '@/store'; +import { + selectBucketDailyStorage, + selectFilterBuckets, + setBucketFilter, +} from '@/store/slices/dashboard'; import { formatChartTime, mergeArr } from '@/utils/dashboard'; import { formatBytes } from '@/utils/formatter'; import { getUtcDayjs } from '@/utils/time'; @@ -11,23 +14,22 @@ import { Box, Flex } from '@node-real/uikit'; import { isEmpty } from 'lodash-es'; import { useMemo } from 'react'; import { LABEL_STYLES, VALUE_STYLES } from '../constants'; -import { Card, CardTitle } from './Common'; -import { FilterBuckets } from './FilterBuckets'; +import { BucketsFilter } from './BucketsFilter'; -export const BucketStorageChart = () => { +export const BucketStorageUsage = () => { + const dispatch = useAppDispatch(); const loginAccount = useAppSelector((root) => root.persist.loginAccount); const filterBuckets = useAppSelector(selectFilterBuckets()); - const bucketDailyStorageRecords = useAppSelector( - (root) => root.dashboard.bucketDailyStorageRecords, - ); - - const bucketDailyStorage = bucketDailyStorageRecords[loginAccount]; + const bucketDailyStorage = useAppSelector(selectBucketDailyStorage()); + const bucketNames = bucketDailyStorage.map((item) => item.BucketName); const isLoading = bucketDailyStorage === undefined; const dayjs = getUtcDayjs(); const noData = !isLoading && isEmpty(bucketDailyStorage); + const onBucketFiltered = (bucketNames: string[]) => { + dispatch(setBucketFilter({ loginAccount, bucketNames })); + }; const lineOptions = useMemo(() => { - // line data according to day to generate; const data = bucketDailyStorage || []; const filterData = isEmpty(filterBuckets) ? data @@ -68,6 +70,15 @@ export const BucketStorageChart = () => { `; }, }, + legend: { + icon: 'circle', + itemHeight: 8, + itemWidth: 8, + itemGap: 16, + right: 12, + textStyle: { fontWeight: 400 }, + data: ['Storage Usage', 'Quota Usage'], + }, xAxis: { data: xData, }, @@ -80,6 +91,18 @@ export const BucketStorageChart = () => { }, series: [ { + symbolSize: 5, + lineStyle: { color: '#00BA34' }, + itemStyle: { + color: '#00BA34', + opacity: 1, + }, + emphasis: { itemStyle: { opacity: 1 } }, + animationDuration: 600, + name: 'Storage Usage', + type: 'line', + smooth: false, + stack: 'Storage Usage', data: yData, }, ], @@ -87,20 +110,13 @@ export const BucketStorageChart = () => { }, [bucketDailyStorage, dayjs, filterBuckets]); return ( - - - Usage Statistics - - - Storage Usage - - - Download Quota Usage - - - + - + @@ -108,6 +124,6 @@ export const BucketStorageChart = () => { {!isLoading && } - + ); }; diff --git a/apps/dcellar-web-ui/src/modules/dashboard/components/BucketUsageStatistics.tsx b/apps/dcellar-web-ui/src/modules/dashboard/components/BucketUsageStatistics.tsx new file mode 100644 index 00000000..cc3e83c6 --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/dashboard/components/BucketUsageStatistics.tsx @@ -0,0 +1,36 @@ +import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@node-real/uikit'; +import { Card, CardTitle } from './Common'; +import { BucketStorageUsage } from './BucketStorageUsage'; +import { BucketQuotaUsage } from './BucketQuotaUsage'; +import { useState } from 'react'; + +export const BucketUsageStatistics = () => { + const [activeKey, setActiveKey] = useState(0); + const onChange = (key: number | string) => { + setActiveKey(key); + }; + return ( + + + + Usage Statistics + + Storage Usage + Download Quota Usage + + + + + {+activeKey === 0 && } + {+activeKey === 1 && } + + + + ); +}; diff --git a/apps/dcellar-web-ui/src/modules/dashboard/components/FilterBuckets.tsx b/apps/dcellar-web-ui/src/modules/dashboard/components/BucketsFilter.tsx similarity index 74% rename from apps/dcellar-web-ui/src/modules/dashboard/components/FilterBuckets.tsx rename to apps/dcellar-web-ui/src/modules/dashboard/components/BucketsFilter.tsx index 6ba1ed71..ce2440c4 100644 --- a/apps/dcellar-web-ui/src/modules/dashboard/components/FilterBuckets.tsx +++ b/apps/dcellar-web-ui/src/modules/dashboard/components/BucketsFilter.tsx @@ -5,54 +5,45 @@ import { DCMenu } from '@/components/common/DCMenu'; import { MenuOption } from '@/components/common/DCMenuList'; import { InputItem } from '@/components/formitems/InputItem'; import { Badge, MenuFooter, MenuHeader } from '@/modules/accounts/components/Common'; -import { useAppDispatch, useAppSelector } from '@/store'; -import { - selectBucketDailyStorage, - selectFilterBuckets, - setBucketFilter, -} from '@/store/slices/dashboard'; import { trimLongStr } from '@/utils/string'; import { SearchIcon } from '@node-real/icons'; import { InputLeftElement, MenuButton, Text, Tooltip } from '@node-real/uikit'; import cn from 'classnames'; import { xor } from 'lodash-es'; -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; -export const FilterBuckets = () => { - const dispatch = useAppDispatch(); - const loginAccount = useAppSelector((root) => root.persist.loginAccount); +export type BucketsFilterProps = { + bucketNames: string[]; + filteredBuckets: string[]; + onBucketFiltered: (bucketNames: string[]) => void; +}; - const router = useRouter(); - const bucketDailyStorage = useAppSelector(selectBucketDailyStorage()); - const filterBuckets = useAppSelector(selectFilterBuckets()); +export const BucketsFilter = ({ + filteredBuckets, + bucketNames, + onBucketFiltered, +}: BucketsFilterProps) => { const [nameFilter, setNameFilter] = useState(''); - const [selectedBucket, setSelectedBucket] = useState>([]); + const [selectedBucket, setSelectedBucket] = useState>(filteredBuckets); const nameToOptions = (name: string) => ({ label: name, value: name, }); - const bucketNames = bucketDailyStorage.map((item) => item.BucketName); - // bucket name will 63 characters const names = bucketNames.filter((name) => !nameFilter.trim() ? true : name.toLowerCase().includes(nameFilter.trim().toLowerCase()), ); const typeOptions: MenuOption[] = names.map(nameToOptions); - const selectedTypeOptions = filterBuckets.map(nameToOptions); + const selectedTypeOptions = filteredBuckets.map(nameToOptions); - const accountClose = () => { - dispatch(setBucketFilter({ loginAccount, buckets: selectedBucket })); - }; + const onSelectClose = useCallback(() => { + onBucketFiltered(selectedBucket); + }, [onBucketFiltered, selectedBucket]); - const accountOpen = () => { - setSelectedBucket(filterBuckets); + const onSelectOpen = () => { + setSelectedBucket(filteredBuckets); }; - useEffect(() => { - setSelectedBucket(filterBuckets); - }, [router.asPath]); - return ( { minH: 226, }} scrollH={150} - onClose={accountClose} - onOpen={accountOpen} + onClose={() => onSelectClose()} + onOpen={() => onSelectOpen()} renderHeader={() => ( { )} renderOption={({ label, value }) => ( { + return selectedBucket.includes(value); + })()} onClick={(e) => { e.stopPropagation(); setSelectedBucket(xor(selectedBucket, [value])); @@ -111,7 +104,7 @@ export const FilterBuckets = () => { > { onClick={(e) => { e.stopPropagation(); setSelectedBucket([]); - dispatch(setBucketFilter({ loginAccount, buckets: [] })); + onBucketFiltered([]); }} className={'icon-selected'} w={24} @@ -142,7 +135,7 @@ export const FilterBuckets = () => { ) : ( <> {trimLongStr(selectedTypeOptions[0].label, 6, 6, 0)}{' '} - {filterBuckets.length} + {filteredBuckets.length} )} diff --git a/apps/dcellar-web-ui/src/modules/dashboard/index.tsx b/apps/dcellar-web-ui/src/modules/dashboard/index.tsx index 0ae597c8..f0d2aa76 100644 --- a/apps/dcellar-web-ui/src/modules/dashboard/index.tsx +++ b/apps/dcellar-web-ui/src/modules/dashboard/index.tsx @@ -4,16 +4,16 @@ import { TotalBalance } from './components/TotalBalance'; import { Stats } from './components/Stats'; import { CurMonthCost } from '../accounts/components/CurMonthCost'; import { CurForecastCost } from '../accounts/components/CurForecastCost'; -import { BucketStorageChart } from './components/BucketStorageChart'; import { useAppDispatch, useAppSelector } from '@/store'; import { setupOwnerAccount, setupPaymentAccounts } from '@/store/slices/accounts'; import { setupAllCostTrend, setupTotalCost } from '@/store/slices/billing'; import { useMount } from 'ahooks'; import { setupBucketList } from '@/store/slices/bucket'; -import { setupBucketDailyStorage } from '@/store/slices/dashboard'; +import { setupBucketDailyQuotaUsage, setupBucketDailyStorageUsage } from '@/store/slices/dashboard'; import { getCurMonthDetailUrl } from '@/utils/accounts'; import { useRouter } from 'next/router'; import { TutorialCard } from './components/TutorialCard'; +import { BucketUsageStatistics } from './components/BucketUsageStatistics'; export const Dashboard = () => { const dispatch = useAppDispatch(); @@ -32,7 +32,8 @@ export const Dashboard = () => { dispatch(setupAllCostTrend()); dispatch(setupPaymentAccounts()); dispatch(setupBucketList(loginAccount)); - dispatch(setupBucketDailyStorage()); + dispatch(setupBucketDailyStorageUsage()); + dispatch(setupBucketDailyQuotaUsage()); }); return ( @@ -62,7 +63,7 @@ export const Dashboard = () => { )} - + {!isLessThan1200 && ( diff --git a/apps/dcellar-web-ui/src/store/slices/dashboard.ts b/apps/dcellar-web-ui/src/store/slices/dashboard.ts index 513d4741..dcebe0c8 100644 --- a/apps/dcellar-web-ui/src/store/slices/dashboard.ts +++ b/apps/dcellar-web-ui/src/store/slices/dashboard.ts @@ -1,15 +1,25 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { AppDispatch, AppState, GetState } from '..'; -import { DailyBucketStorage, getDailyBucketStorageListByOwner } from '@/facade/explorer'; +import { + BucketDailyStorageUsage, + getBucketDailyStorageUsage, + getBucketDailyQuotaUsage, + BucketDailyQuotaUsage, +} from '@/facade/dashboard'; interface DashboardState { bucketFilterRecords: Record; - bucketDailyStorageRecords: Record; + bucketQuotaUsageFilterRecords: Record; + bucketDailyStorageUsageRecords: Record; + // The first key is loginAccount, the second key is bucketName + bucketDailyQuotaUsageRecords: Record>; } const initialState: DashboardState = { bucketFilterRecords: {}, - bucketDailyStorageRecords: {}, + bucketQuotaUsageFilterRecords: {}, + bucketDailyStorageUsageRecords: {}, + bucketDailyQuotaUsageRecords: {}, }; export const dashboardSlice = createSlice({ @@ -18,24 +28,48 @@ export const dashboardSlice = createSlice({ reducers: { setBucketFilter( state, - { payload }: PayloadAction<{ loginAccount: string; buckets: string[] }>, + { payload }: PayloadAction<{ loginAccount: string; bucketNames: string[] }>, ) { - const { loginAccount, buckets } = payload; - state.bucketFilterRecords[loginAccount] = buckets; + const { loginAccount, bucketNames } = payload; + state.bucketFilterRecords[loginAccount] = bucketNames; }, - setBucketDailyStorage( + setBucketDailyQuotaFilter( + state, + { payload }: PayloadAction<{ loginAccount: string; bucketNames: string[] }>, + ) { + const { loginAccount, bucketNames } = payload; + state.bucketQuotaUsageFilterRecords[loginAccount] = bucketNames; + }, + setBucketDailyStorageUsage( state, { payload, - }: PayloadAction<{ loginAccount: string; bucketDailyStorage: DailyBucketStorage[] }>, + }: PayloadAction<{ loginAccount: string; bucketDailyStorage: BucketDailyStorageUsage[] }>, ) { const { loginAccount, bucketDailyStorage } = payload; - state.bucketDailyStorageRecords[loginAccount] = bucketDailyStorage; + state.bucketDailyStorageUsageRecords[loginAccount] = bucketDailyStorage; + }, + setBucketDailyQuotaUsage( + state, + { + payload, + }: PayloadAction<{ + loginAccount: string; + bucketDailyQuotaUsage: Record; + }>, + ) { + const { loginAccount, bucketDailyQuotaUsage } = payload; + state.bucketDailyQuotaUsageRecords[loginAccount] = bucketDailyQuotaUsage; }, }, }); -export const { setBucketFilter, setBucketDailyStorage } = dashboardSlice.actions; +export const { + setBucketFilter, + setBucketDailyQuotaFilter, + setBucketDailyStorageUsage, + setBucketDailyQuotaUsage, +} = dashboardSlice.actions; const defaultFilterBuckets: string[] = []; export const selectFilterBuckets = () => (root: AppState) => { @@ -43,22 +77,61 @@ export const selectFilterBuckets = () => (root: AppState) => { return root.dashboard.bucketFilterRecords[loginAccount] || defaultFilterBuckets; }; -const defaultBucketDailyStorage = [] as DailyBucketStorage[]; +const defaultFilterQuotaUsageBuckets: string[] = []; +export const selectFilterQuotaUsageBuckets = () => (root: AppState) => { + const loginAccount = root.persist.loginAccount; + return ( + root.dashboard.bucketQuotaUsageFilterRecords[loginAccount] || defaultFilterQuotaUsageBuckets + ); +}; + +const defaultBucketDailyStorage = [] as BucketDailyStorageUsage[]; export const selectBucketDailyStorage = () => (root: AppState) => { const loginAccount = root.persist.loginAccount; - return root.dashboard.bucketDailyStorageRecords[loginAccount] || defaultBucketDailyStorage; + return root.dashboard.bucketDailyStorageUsageRecords[loginAccount] || defaultBucketDailyStorage; }; -export const setupBucketDailyStorage = () => async (dispatch: AppDispatch, getState: GetState) => { - const { loginAccount } = getState().persist; - const params = { page: 1, per_page: 201, owner: loginAccount }; - const [data, error] = await getDailyBucketStorageListByOwner(params); +const defaultBucketDailyQuota = [] as BucketDailyQuotaUsage[]; +export const selectBucketDailyQuotaUsage = () => (root: AppState) => { + const loginAccount = root.persist.loginAccount; + return root.dashboard.bucketDailyQuotaUsageRecords[loginAccount] || defaultBucketDailyQuota; +}; - if (error || data === null) { - return dispatch(setBucketDailyStorage({ loginAccount, bucketDailyStorage: [] })); - } +export const setupBucketDailyStorageUsage = + () => async (dispatch: AppDispatch, getState: GetState) => { + const { loginAccount } = getState().persist; + const params = { page: 1, per_page: 201, owner: loginAccount }; + const [data, error] = await getBucketDailyStorageUsage(params); - dispatch(setBucketDailyStorage({ loginAccount, bucketDailyStorage: data })); -}; + if (error || data === null) { + return dispatch(setBucketDailyStorageUsage({ loginAccount, bucketDailyStorage: [] })); + } + + dispatch(setBucketDailyStorageUsage({ loginAccount, bucketDailyStorage: data })); + }; + +export const setupBucketDailyQuotaUsage = + () => async (dispatch: AppDispatch, getState: GetState) => { + const { loginAccount } = getState().persist; + const params = { page: 1, per_page: 201, owner: loginAccount }; + const [data, error] = await getBucketDailyQuotaUsage(params); + + if (error || data === null) { + return dispatch(setBucketDailyQuotaUsage({ loginAccount, bucketDailyQuotaUsage: {} })); + } + const formatData = Object.entries(data).reduce( + (acc, [key, value]) => { + const bucketName = data[key][0].BucketName; + if (!acc[bucketName]) { + acc[bucketName] = []; + } + acc[bucketName] = value; + return acc; + }, + {} as Record, + ); + + dispatch(setBucketDailyQuotaUsage({ loginAccount, bucketDailyQuotaUsage: formatData })); + }; export default dashboardSlice.reducer; diff --git a/apps/dcellar-web-ui/src/utils/dashboard/index.ts b/apps/dcellar-web-ui/src/utils/dashboard/index.ts index 03d35304..1770d689 100644 --- a/apps/dcellar-web-ui/src/utils/dashboard/index.ts +++ b/apps/dcellar-web-ui/src/utils/dashboard/index.ts @@ -5,6 +5,12 @@ export function formatChartTime(time: string | number) { return formatDateUTC(time, 'dddd, MMM DD, YYYY'); } +/** + * Merge two string arrays, add elements at corresponding positions, and return a new array + * @param {string[]} arr1 - The first string array + * @param {string[]} arr2 - The second string array + * @returns {string[]} - The merged new array + */ export const mergeArr = (arr1: string[], arr2: string[]) => { const diffLen = arr1.length - arr2.length; const sameLenArr1 = diffLen >= 0 ? arr1 : new Array(Math.abs(diffLen)).fill(0).concat(arr1); From a89c0591d3169d02f5a9fe953943cf858e777eee Mon Sep 17 00:00:00 2001 From: devinxl Date: Tue, 4 Jun 2024 11:05:51 +0800 Subject: [PATCH 2/4] fix(dcellar-web-ui): total quota usage cal error --- .../dashboard/components/BucketQuotaUsage.tsx | 34 +++++++++---------- .../src/store/slices/dashboard.ts | 3 ++ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/apps/dcellar-web-ui/src/modules/dashboard/components/BucketQuotaUsage.tsx b/apps/dcellar-web-ui/src/modules/dashboard/components/BucketQuotaUsage.tsx index f66972e2..90da7571 100644 --- a/apps/dcellar-web-ui/src/modules/dashboard/components/BucketQuotaUsage.tsx +++ b/apps/dcellar-web-ui/src/modules/dashboard/components/BucketQuotaUsage.tsx @@ -31,40 +31,38 @@ export const BucketQuotaUsage = () => { dispatch(setBucketDailyQuotaFilter({ loginAccount, bucketNames })); }; - const groupDataByTime = useMemo(() => { + const quotaUsageByTime = useMemo(() => { const data = bucketDailyQuotaUsage || {}; const filterData = isEmpty(filteredBuckets) ? data : filteredBuckets.map((item) => data[item]).filter((item) => item !== undefined); - const groupDataByTime: Record< + const quotaUsageByTime: Record< string, { MonthlyQuotaSize: string; MonthlyQuotaConsumedSize: string } > = {}; Object.values(filterData).forEach((quotaUsages) => { quotaUsages.forEach((item) => { - if (!groupDataByTime[item.Date]) { - groupDataByTime[item.Date] = { + if (!quotaUsageByTime[item.Date]) { + return (quotaUsageByTime[item.Date] = { MonthlyQuotaSize: String(item.MonthlyQuotaSize), MonthlyQuotaConsumedSize: String(item.MonthlyQuotaConsumedSize), - }; - } - if (groupDataByTime[item.Date]) { - groupDataByTime[item.Date] = { - MonthlyQuotaSize: BN(groupDataByTime[item.Date].MonthlyQuotaSize) - .plus(item.MonthlyQuotaSize) - .toString(), - MonthlyQuotaConsumedSize: BN(groupDataByTime[item.Date].MonthlyQuotaConsumedSize) - .plus(BN(item.MonthlyQuotaConsumedSize)) - .toString(), - }; + }); } + quotaUsageByTime[item.Date] = { + MonthlyQuotaSize: BN(quotaUsageByTime[item.Date].MonthlyQuotaSize) + .plus(item.MonthlyQuotaSize) + .toString(), + MonthlyQuotaConsumedSize: BN(quotaUsageByTime[item.Date].MonthlyQuotaConsumedSize) + .plus(BN(item.MonthlyQuotaConsumedSize)) + .toString(), + }; }); }); - return groupDataByTime; + return quotaUsageByTime; }, [bucketDailyQuotaUsage, filteredBuckets]); const lineOptions = useMemo(() => { - const lineData = Object.entries(groupDataByTime).map(([time, quotaData]) => ({ + const lineData = Object.entries(quotaUsageByTime).map(([time, quotaData]) => ({ time: getMillisecond(+time), totalQuota: quotaData.MonthlyQuotaSize, quotaUsage: quotaData.MonthlyQuotaConsumedSize, @@ -151,7 +149,7 @@ export const BucketQuotaUsage = () => { }, ], }; - }, [groupDataByTime, dayjs]); + }, [quotaUsageByTime, dayjs]); return ( diff --git a/apps/dcellar-web-ui/src/store/slices/dashboard.ts b/apps/dcellar-web-ui/src/store/slices/dashboard.ts index dcebe0c8..401fab4a 100644 --- a/apps/dcellar-web-ui/src/store/slices/dashboard.ts +++ b/apps/dcellar-web-ui/src/store/slices/dashboard.ts @@ -121,6 +121,9 @@ export const setupBucketDailyQuotaUsage = } const formatData = Object.entries(data).reduce( (acc, [key, value]) => { + if (!data[key]) { + return acc; + } const bucketName = data[key][0].BucketName; if (!acc[bucketName]) { acc[bucketName] = []; From 8091b66755409dd12fa3c2b8569a76a33107a956 Mon Sep 17 00:00:00 2001 From: devinxl Date: Tue, 4 Jun 2024 11:27:03 +0800 Subject: [PATCH 3/4] fix(dcellar-web-ui): add loading for the dashboard charts --- .../dashboard/components/BucketQuotaUsage.tsx | 14 +++++++------- .../dashboard/components/BucketStorageUsage.tsx | 13 ++++++------- apps/dcellar-web-ui/src/store/slices/dashboard.ts | 12 ------------ 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/apps/dcellar-web-ui/src/modules/dashboard/components/BucketQuotaUsage.tsx b/apps/dcellar-web-ui/src/modules/dashboard/components/BucketQuotaUsage.tsx index 90da7571..09237e23 100644 --- a/apps/dcellar-web-ui/src/modules/dashboard/components/BucketQuotaUsage.tsx +++ b/apps/dcellar-web-ui/src/modules/dashboard/components/BucketQuotaUsage.tsx @@ -3,29 +3,29 @@ import { Loading } from '@/components/common/Loading'; import { FilterContainer } from '@/modules/accounts/components/Common'; import { useAppDispatch, useAppSelector } from '@/store'; import { setBucketDailyQuotaFilter } from '@/store/slices/dashboard'; -import { formatChartTime, mergeArr } from '@/utils/dashboard'; +import { formatChartTime } from '@/utils/dashboard'; import { formatBytes } from '@/utils/formatter'; import { getMillisecond, getUtcDayjs } from '@/utils/time'; import { Box, Flex } from '@node-real/uikit'; import { isEmpty } from 'lodash-es'; import { useMemo } from 'react'; import { LABEL_STYLES, VALUE_STYLES } from '../constants'; -import { - selectBucketDailyQuotaUsage, - selectFilterQuotaUsageBuckets, -} from '@/store/slices/dashboard'; +import { selectFilterQuotaUsageBuckets } from '@/store/slices/dashboard'; import { BN } from '@/utils/math'; import { BucketsFilter } from './BucketsFilter'; export const BucketQuotaUsage = () => { const dispatch = useAppDispatch(); const loginAccount = useAppSelector((root) => root.persist.loginAccount); - const bucketDailyQuotaUsage = useAppSelector(selectBucketDailyQuotaUsage()); + const bucketDailyQuotaUsageRecords = useAppSelector( + (root) => root.dashboard.bucketDailyQuotaUsageRecords, + ); + const bucketDailyQuotaUsage = bucketDailyQuotaUsageRecords[loginAccount]; const filteredBuckets = useAppSelector(selectFilterQuotaUsageBuckets()); const isLoading = bucketDailyQuotaUsage === undefined; const dayjs = getUtcDayjs(); const noData = !isLoading && isEmpty(bucketDailyQuotaUsage); - const bucketNames = Object.keys(bucketDailyQuotaUsage); + const bucketNames = (!isLoading && Object.keys(bucketDailyQuotaUsage)) || ['']; const onBucketFiltered = (bucketNames: string[]) => { dispatch(setBucketDailyQuotaFilter({ loginAccount, bucketNames })); diff --git a/apps/dcellar-web-ui/src/modules/dashboard/components/BucketStorageUsage.tsx b/apps/dcellar-web-ui/src/modules/dashboard/components/BucketStorageUsage.tsx index b09ad5f9..4c4d6157 100644 --- a/apps/dcellar-web-ui/src/modules/dashboard/components/BucketStorageUsage.tsx +++ b/apps/dcellar-web-ui/src/modules/dashboard/components/BucketStorageUsage.tsx @@ -2,11 +2,7 @@ import { LineChart } from '@/components/charts/LineChart'; import { Loading } from '@/components/common/Loading'; import { FilterContainer } from '@/modules/accounts/components/Common'; import { useAppDispatch, useAppSelector } from '@/store'; -import { - selectBucketDailyStorage, - selectFilterBuckets, - setBucketFilter, -} from '@/store/slices/dashboard'; +import { selectFilterBuckets, setBucketFilter } from '@/store/slices/dashboard'; import { formatChartTime, mergeArr } from '@/utils/dashboard'; import { formatBytes } from '@/utils/formatter'; import { getUtcDayjs } from '@/utils/time'; @@ -20,9 +16,12 @@ export const BucketStorageUsage = () => { const dispatch = useAppDispatch(); const loginAccount = useAppSelector((root) => root.persist.loginAccount); const filterBuckets = useAppSelector(selectFilterBuckets()); - const bucketDailyStorage = useAppSelector(selectBucketDailyStorage()); - const bucketNames = bucketDailyStorage.map((item) => item.BucketName); + const bucketDailyStorageRecords = useAppSelector( + (root) => root.dashboard.bucketDailyStorageUsageRecords, + ); + const bucketDailyStorage = bucketDailyStorageRecords[loginAccount]; const isLoading = bucketDailyStorage === undefined; + const bucketNames = !isLoading ? bucketDailyStorage.map((item) => item.BucketName) : ['']; const dayjs = getUtcDayjs(); const noData = !isLoading && isEmpty(bucketDailyStorage); const onBucketFiltered = (bucketNames: string[]) => { diff --git a/apps/dcellar-web-ui/src/store/slices/dashboard.ts b/apps/dcellar-web-ui/src/store/slices/dashboard.ts index 401fab4a..49b8674d 100644 --- a/apps/dcellar-web-ui/src/store/slices/dashboard.ts +++ b/apps/dcellar-web-ui/src/store/slices/dashboard.ts @@ -85,18 +85,6 @@ export const selectFilterQuotaUsageBuckets = () => (root: AppState) => { ); }; -const defaultBucketDailyStorage = [] as BucketDailyStorageUsage[]; -export const selectBucketDailyStorage = () => (root: AppState) => { - const loginAccount = root.persist.loginAccount; - return root.dashboard.bucketDailyStorageUsageRecords[loginAccount] || defaultBucketDailyStorage; -}; - -const defaultBucketDailyQuota = [] as BucketDailyQuotaUsage[]; -export const selectBucketDailyQuotaUsage = () => (root: AppState) => { - const loginAccount = root.persist.loginAccount; - return root.dashboard.bucketDailyQuotaUsageRecords[loginAccount] || defaultBucketDailyQuota; -}; - export const setupBucketDailyStorageUsage = () => async (dispatch: AppDispatch, getState: GetState) => { const { loginAccount } = getState().persist; From 577e67690ebc1796aa699e781d8aa00ea8f422e7 Mon Sep 17 00:00:00 2001 From: devinxl Date: Thu, 6 Jun 2024 11:15:00 +0800 Subject: [PATCH 4/4] docs(dcellar-web-ui): update changelog --- apps/dcellar-web-ui/CHANGELOG.json | 12 ++++++++++++ apps/dcellar-web-ui/CHANGELOG.md | 9 ++++++++- apps/dcellar-web-ui/package.json | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/apps/dcellar-web-ui/CHANGELOG.json b/apps/dcellar-web-ui/CHANGELOG.json index e117c00b..a91c4357 100644 --- a/apps/dcellar-web-ui/CHANGELOG.json +++ b/apps/dcellar-web-ui/CHANGELOG.json @@ -1,6 +1,18 @@ { "name": "dcellar-web-ui", "entries": [ + { + "version": "1.7.0", + "tag": "dcellar-web-ui_v1.7.0", + "date": "Thu, 06 Jun 2024 03:14:47 GMT", + "comments": { + "minor": [ + { + "comment": "Add the download quota usage chart" + } + ] + } + }, { "version": "1.6.1", "tag": "dcellar-web-ui_v1.6.1", diff --git a/apps/dcellar-web-ui/CHANGELOG.md b/apps/dcellar-web-ui/CHANGELOG.md index 332c3db0..4dff0ff4 100644 --- a/apps/dcellar-web-ui/CHANGELOG.md +++ b/apps/dcellar-web-ui/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log - dcellar-web-ui -This log was last generated on Thu, 30 May 2024 10:00:54 GMT and should not be manually modified. +This log was last generated on Thu, 06 Jun 2024 03:14:47 GMT and should not be manually modified. + +## 1.7.0 +Thu, 06 Jun 2024 03:14:47 GMT + +### Minor changes + +- Add the download quota usage chart ## 1.6.1 Thu, 30 May 2024 10:00:54 GMT diff --git a/apps/dcellar-web-ui/package.json b/apps/dcellar-web-ui/package.json index f1f65f71..26de4f62 100644 --- a/apps/dcellar-web-ui/package.json +++ b/apps/dcellar-web-ui/package.json @@ -1,6 +1,6 @@ { "name": "dcellar-web-ui", - "version": "1.6.1", + "version": "1.7.0", "private": false, "scripts": { "dev": "node ./scripts/dev.js -p 3200",