Skip to content

Commit

Permalink
feat(1-3262): initial impl of new month/range picker (#9122)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
thomasheartman authored Jan 21, 2025
1 parent 08a28c9 commit 857c91b
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -269,30 +279,49 @@ export const NetworkTrafficUsage: VFC = () => {
}
/>
<StyledBox>
<Grid container component='header' spacing={2}>
<Grid item xs={12} md={10}>
{showMultiMonthSelector ? (
<NewHeader>
<NetworkTrafficUsagePlanSummary
usageTotal={usageTotal}
includedTraffic={includedTraffic}
overageCost={overageCost}
estimatedMonthlyCost={estimatedMonthlyCost}
/>
</Grid>
<Grid item xs={12} md={2}>
<Select
id='dataperiod-select'
name='dataperiod'
options={selectablePeriods}
value={period}
onChange={(e) => setPeriod(e.target.value)}
style={{
minWidth: '100%',
marginBottom: theme.spacing(2),
}}
formControlStyles={{ width: '100%' }}
<PeriodSelector
selectedPeriod={period}
setPeriod={setPeriod}
/>
</NewHeader>
) : (
<Grid container component='header' spacing={2}>
<Grid item xs={12} md={10}>
<NetworkTrafficUsagePlanSummary
usageTotal={usageTotal}
includedTraffic={includedTraffic}
overageCost={overageCost}
estimatedMonthlyCost={
estimatedMonthlyCost
}
/>
</Grid>
<Grid item xs={12} md={2}>
<Select
id='dataperiod-select'
name='dataperiod'
options={selectablePeriods}
value={period}
onChange={(e) =>
setPeriod(e.target.value)
}
style={{
minWidth: '100%',
marginBottom: theme.spacing(2),
}}
formControlStyles={{ width: '100%' }}
/>
</Grid>
</Grid>
</Grid>
)}
<Grid item xs={12} md={2}>
<Bar
data={data}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { styled } from '@mui/material';
import { type FC, useState } from 'react';

export type Period = {
key: string;
dayCount: number;
label: string;
year: number;
month: number;
selectable: boolean;
shortLabel: string;
};

export const toSelectablePeriod = (
date: Date,
label?: string,
selectable = true,
): Period => {
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<Props> = ({ 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<Selection | null>();

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 (
<Wrapper>
<MonthSelector>
<hgroup>
<h3>Select month</h3>
<p>Last 12 months</p>
</hgroup>
<MonthGrid>
{selectablePeriods.map((period, index) => (
<li key={period.label}>
<button
className={
!tempOverride &&
period.key === selectedPeriod
? 'selected'
: ''
}
type='button'
disabled={!period.selectable}
onClick={() => {
select({
type: 'month',
value: period.key,
});
}}
>
{period.shortLabel}
</button>
</li>
))}
</MonthGrid>
</MonthSelector>
<RangeSelector>
<h4>Range</h4>

<RangeList>
{rangeOptions.map((option) => (
<li key={option.label}>
<button
className={
tempOverride &&
tempOverride.type === 'range' &&
option.value === tempOverride.monthsBack
? 'selected'
: ''
}
type='button'
onClick={() => {
select({
type: 'range',
monthsBack: option.value,
});
}}
>
Last {option.value} months
</button>
</li>
))}
</RangeList>
</RangeSelector>
</Wrapper>
);
};
3 changes: 1 addition & 2 deletions frontend/src/hooks/useTrafficData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/interfaces/uiConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export type UiFlags = {
sortProjectRoles?: boolean;
lifecycleImprovements?: boolean;
frontendHeaderRedesign?: boolean;
dataUsageMultiMonthView?: boolean;
};

export interface IVersionInfo {
Expand Down
7 changes: 6 additions & 1 deletion src/lib/types/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ export type IFlagKey =
| 'uniqueSdkTracking'
| 'sortProjectRoles'
| 'lifecycleImprovements'
| 'frontendHeaderRedesign';
| 'frontendHeaderRedesign'
| 'dataUsageMultiMonthView';

export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;

Expand Down Expand Up @@ -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 = {
Expand Down
1 change: 1 addition & 0 deletions src/server-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ process.nextTick(async () => {
uniqueSdkTracking: true,
lifecycleImprovements: true,
frontendHeaderRedesign: true,
dataUsageMultiMonthView: true,
},
},
authentication: {
Expand Down

0 comments on commit 857c91b

Please sign in to comment.