From 857c91b803d5120a89a4d9f159aeafe434dbb8f9 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 21 Jan 2025 12:15:43 +0100 Subject: [PATCH] feat(1-3262): initial impl of new month/range picker (#9122) This PR implements a first version of the new month/range picker for the data usage graphs. It's minimally hooked up to the existing functionality to not take anything away. This primary purpose of this PR is to get the design and interaction out on sandbox so that UX can have a look and we can make adjustments. As such, there are a few things in the code that we'll want to clean up before removing the flag later: - for faster iteration, I've used a lot of CSS nesting and element selectors. this isn't usually how we do it here, so we'll probably want to extract into styled components later - there is a temporary override of the value in the period selector so that you can select ranges. It won't affect the chart state, but it affects the selector state. Again, this lets you see how it acts and works. - I've added a `NewHeader` component because the existing setup smushed the selector (it's a MUI grid setup, which isn't very flexible). I don't know what we want to do with this in the end, but the existing chart *does* have some problems when you resize your window, at least (although this is likely due to the chart, and can be solved in the same way that we did for the personal dashboards). ![image](https://github.com/user-attachments/assets/f3ce3ff9-bab3-4d00-afbe-56f5624fbe16) --- .../NetworkTrafficUsage.tsx | 65 +++-- .../NetworkTrafficUsage/PeriodSelector.tsx | 230 ++++++++++++++++++ frontend/src/hooks/useTrafficData.ts | 3 +- frontend/src/interfaces/uiConfig.ts | 1 + src/lib/types/experimental.ts | 7 +- src/server-dev.ts | 1 + 6 files changed, 286 insertions(+), 21 deletions(-) create mode 100644 frontend/src/component/admin/network/NetworkTrafficUsage/PeriodSelector.tsx 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: {