mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-28 00:17:12 +01:00
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). 
This commit is contained in:
parent
08a28c99d6
commit
857c91b803
@ -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 = () => {
|
||||
}
|
||||
/>
|
||||
<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}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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,
|
||||
|
@ -93,6 +93,7 @@ export type UiFlags = {
|
||||
sortProjectRoles?: boolean;
|
||||
lifecycleImprovements?: boolean;
|
||||
frontendHeaderRedesign?: boolean;
|
||||
dataUsageMultiMonthView?: boolean;
|
||||
};
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
@ -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 = {
|
||||
|
@ -58,6 +58,7 @@ process.nextTick(async () => {
|
||||
uniqueSdkTracking: true,
|
||||
lifecycleImprovements: true,
|
||||
frontendHeaderRedesign: true,
|
||||
dataUsageMultiMonthView: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
Loading…
Reference in New Issue
Block a user