diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx index 77ac241778..113ccd5926 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx @@ -1,4 +1,4 @@ -import { useMemo, type VFC, useState, useEffect } from 'react'; +import { useMemo, useState, useEffect, type FC } from 'react'; import useTheme from '@mui/material/styles/useTheme'; import styled from '@mui/material/styles/styled'; import { usePageTitle } from 'hooks/usePageTitle'; @@ -34,6 +34,8 @@ import { formatTickValue } from 'component/common/Chart/formatTickValue'; import { useTrafficLimit } from './hooks/useTrafficLimit'; import { BILLING_TRAFFIC_BUNDLE_PRICE } from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan'; import { useLocationSettings } from 'hooks/useLocationSettings'; +import { PeriodSelector } from './PeriodSelector'; +import { useUiFlag } from 'hooks/useUiFlag'; const StyledBox = styled(Box)(({ theme }) => ({ display: 'grid', @@ -139,9 +141,17 @@ const createBarChartOptions = ( }, }); -export const NetworkTrafficUsage: VFC = () => { +// this is primarily for dev purposes. The existing grid is very inflexible, so we might want to change it, but for demoing the design, this is enough. +const NewHeader = styled('div')(() => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'flex-start', +})); + +export const NetworkTrafficUsage: FC = () => { usePageTitle('Network - Data Usage'); const theme = useTheme(); + const showMultiMonthSelector = useUiFlag('dataUsageMultiMonthView'); const { isOss } = useUiConfig(); @@ -269,30 +279,49 @@ export const NetworkTrafficUsage: VFC = () => { } /> - - + {showMultiMonthSelector ? ( + - - - + setPeriod(e.target.value) + } + style={{ + minWidth: '100%', + marginBottom: theme.spacing(2), + }} + formControlStyles={{ width: '100%' }} + /> + - + )} { + const year = date.getFullYear(); + const month = date.getMonth(); + const period = `${year}-${(month + 1).toString().padStart(2, '0')}`; + const dayCount = new Date(year, month + 1, 0).getDate(); + return { + key: period, + year, + month, + dayCount, + shortLabel: date.toLocaleString('en-US', { + month: 'short', + }), + label: + label || + date.toLocaleString('en-US', { month: 'long', year: 'numeric' }), + selectable, + }; +}; + +const currentDate = new Date(Date.now()); +const currentPeriod = toSelectablePeriod(currentDate, 'Current month'); + +const getSelectablePeriods = (): Period[] => { + const selectablePeriods = [currentPeriod]; + for ( + let subtractMonthCount = 1; + subtractMonthCount < 12; + subtractMonthCount++ + ) { + // JavaScript wraps around the year, so we don't need to handle that. + const date = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - subtractMonthCount, + 1, + ); + selectablePeriods.push( + toSelectablePeriod(date, undefined, date > new Date('2024-03-31')), + ); + } + return selectablePeriods; +}; + +const Wrapper = styled('article')(({ theme }) => ({ + borderRadius: theme.shape.borderRadiusLarge, + border: `2px solid ${theme.palette.divider}`, + padding: theme.spacing(3), + display: 'flex', + flexFlow: 'column', + gap: theme.spacing(2), + button: { + cursor: 'pointer', + border: 'none', + background: 'none', + fontSize: theme.typography.body1.fontSize, + padding: theme.spacing(0.5), + borderRadius: theme.shape.borderRadius, + + '&.selected': { + backgroundColor: theme.palette.secondary.light, + }, + }, + 'button:disabled': { + cursor: 'default', + }, +})); + +const MonthSelector = styled('article')(({ theme }) => ({ + border: 'none', + hgroup: { + h3: { + margin: 0, + fontSize: theme.typography.h3.fontSize, + }, + p: { + color: theme.palette.text.secondary, + fontSize: theme.typography.body2.fontSize, + }, + + marginBottom: theme.spacing(1), + }, +})); + +const MonthGrid = styled('ul')(({ theme }) => ({ + listStyle: 'none', + padding: 0, + display: 'grid', + gridTemplateColumns: 'repeat(4, 1fr)', + rowGap: theme.spacing(1), + columnGap: theme.spacing(2), +})); + +const RangeSelector = styled('article')(({ theme }) => ({ + display: 'flex', + flexFlow: 'column', + gap: theme.spacing(0.5), + h4: { + fontSize: theme.typography.body2.fontSize, + margin: 0, + color: theme.palette.text.secondary, + }, +})); + +const RangeList = styled('ul')(({ theme }) => ({ + listStyle: 'none', + padding: 0, + 'li + li': { + marginTop: theme.spacing(1), + }, + + button: { + marginLeft: `-${theme.spacing(0.5)}`, + }, +})); + +type Selection = + | { + type: 'month'; + value: string; + } + | { + type: 'range'; + monthsBack: number; + }; + +type Props = { + selectedPeriod: string; + setPeriod: (period: string) => void; +}; + +export const PeriodSelector: FC = ({ selectedPeriod, setPeriod }) => { + const selectablePeriods = getSelectablePeriods(); + + // this is for dev purposes; only to show how the design will work when you select a range. + const [tempOverride, setTempOverride] = useState(); + + const select = (value: Selection) => { + if (value.type === 'month') { + setTempOverride(null); + setPeriod(value.value); + } else { + setTempOverride(value); + } + }; + + const rangeOptions = [3, 6, 12].map((monthsBack) => ({ + value: monthsBack, + label: `Last ${monthsBack} months`, + })); + + return ( + + +
+

Select month

+

Last 12 months

+
+ + {selectablePeriods.map((period, index) => ( +
  • + +
  • + ))} +
    +
    + +

    Range

    + + + {rangeOptions.map((option) => ( +
  • + +
  • + ))} +
    +
    +
    + ); +}; diff --git a/frontend/src/hooks/useTrafficData.ts b/frontend/src/hooks/useTrafficData.ts index 5e8ebd90a3..b2aefe0598 100644 --- a/frontend/src/hooks/useTrafficData.ts +++ b/frontend/src/hooks/useTrafficData.ts @@ -48,8 +48,7 @@ const calculateTrafficDataCost = ( return unitCount * trafficUnitCost; }; -const padMonth = (month: number): string => - month < 10 ? `0${month}` : `${month}`; +const padMonth = (month: number): string => month.toString().padStart(2, '0'); export const toSelectablePeriod = ( date: Date, diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index 1410afbe84..580ad39d54 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -93,6 +93,7 @@ export type UiFlags = { sortProjectRoles?: boolean; lifecycleImprovements?: boolean; frontendHeaderRedesign?: boolean; + dataUsageMultiMonthView?: boolean; }; export interface IVersionInfo { diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 780666dd59..6747474ec1 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -62,7 +62,8 @@ export type IFlagKey = | 'uniqueSdkTracking' | 'sortProjectRoles' | 'lifecycleImprovements' - | 'frontendHeaderRedesign'; + | 'frontendHeaderRedesign' + | 'dataUsageMultiMonthView'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -299,6 +300,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_FRONTEND_HEADER_REDESIGN, false, ), + dataUsageMultiMonthView: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_DATA_USAGE_MULTI_MONTH_VIEW, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = { diff --git a/src/server-dev.ts b/src/server-dev.ts index 72d9c3e04f..948f47de75 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -58,6 +58,7 @@ process.nextTick(async () => { uniqueSdkTracking: true, lifecycleImprovements: true, frontendHeaderRedesign: true, + dataUsageMultiMonthView: true, }, }, authentication: {