Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(1-3262): initial impl of new month/range picker #9122

Merged
merged 8 commits into from
Jan 21, 2025
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 = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me DateRange is more intuitive, and easier to find in the code.

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++
) {
Comment on lines +43 to +47
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing months in reverse order looks a bit off. I'm used to pickers having same natural order, and maybe even starting line aligned to January

// 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;
};
Comment on lines +4 to +59
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions are mostly copied from frontend/src/hooks/useTrafficData.ts, but slightly modified. We'll probably want to reconsider this as the API is built out. I don't want to touch them more than I need to right now.


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,
},
},
thomasheartman marked this conversation as resolved.
Show resolved Hide resolved
'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),
},
thomasheartman marked this conversation as resolved.
Show resolved Hide resolved

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'
: ''
}
Comment on lines +179 to +184
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this with classes + nested selector instead of props makes it harder to refactor. There a link between how this element looks like, and where it is placed.

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');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Javascript can do this for us with a builtin instead of us checking the value


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 @@ -63,7 +63,8 @@ export type IFlagKey =
| 'uniqueSdkTracking'
| 'sortProjectRoles'
| 'lifecycleImprovements'
| 'frontendHeaderRedesign';
| 'frontendHeaderRedesign'
| 'dataUsageMultiMonthView';

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

Expand Down Expand Up @@ -300,6 +301,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
Loading