1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-17 01:17:29 +02:00

feat: executive dashboard responsive grid (#6069)

- unified "Widget" component
- column order dependent on screen width
This commit is contained in:
Tymoteusz Czech 2024-01-30 17:02:13 +01:00 committed by GitHub
parent ccc41dca4e
commit c9ac4916e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 148 additions and 106 deletions

View File

@ -1,24 +1,61 @@
import { Box, Paper, styled, Typography } from '@mui/material'; import { useMemo, VFC } from 'react';
import {
Box,
styled,
Typography,
useMediaQuery,
useTheme,
} from '@mui/material';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { VFC } from 'react';
import { UsersChart } from './UsersChart/UsersChart'; import { UsersChart } from './UsersChart/UsersChart';
import { FlagsChart } from './FlagsChart/FlagsChart'; import { FlagsChart } from './FlagsChart/FlagsChart';
import { useExecutiveDashboard } from 'hooks/api/getters/useExecutiveSummary/useExecutiveSummary'; import { useExecutiveDashboard } from 'hooks/api/getters/useExecutiveSummary/useExecutiveSummary';
import { UserStats } from './UserStats/UserStats'; import { UserStats } from './UserStats/UserStats';
import { FlagStats } from './FlagStats/FlagStats'; import { FlagStats } from './FlagStats/FlagStats';
import { Widget } from './Widget/Widget';
const StyledGrid = styled(Box)(({ theme }) => ({ const StyledGrid = styled(Box)(({ theme }) => ({
display: 'grid', display: 'grid',
gridTemplateColumns: `300px 1fr`, gridTemplateColumns: `300px 1fr`,
// TODO: responsive grid size
gridAutoRows: 'auto', gridAutoRows: 'auto',
gap: theme.spacing(2), gap: theme.spacing(2),
})); }));
const useDashboardGrid = () => {
const theme = useTheme();
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
if (isSmallScreen) {
return {
gridTemplateColumns: `1fr`,
chartSpan: 1,
userTrendsOrder: 3,
flagStatsOrder: 2,
};
}
if (isMediumScreen) {
return {
gridTemplateColumns: `1fr 1fr`,
chartSpan: 2,
userTrendsOrder: 3,
flagStatsOrder: 2,
};
}
return {
gridTemplateColumns: `300px auto`,
chartSpan: 1,
userTrendsOrder: 2,
flagStatsOrder: 3,
};
};
export const ExecutiveDashboard: VFC = () => { export const ExecutiveDashboard: VFC = () => {
const { executiveDashboardData, loading, error } = useExecutiveDashboard(); const { executiveDashboardData, loading, error } = useExecutiveDashboard();
const calculateFlagPerUsers = () => { const flagPerUsers = useMemo(() => {
if ( if (
executiveDashboardData.users.total === 0 || executiveDashboardData.users.total === 0 ||
executiveDashboardData.flags.total === 0 executiveDashboardData.flags.total === 0
@ -29,7 +66,10 @@ export const ExecutiveDashboard: VFC = () => {
executiveDashboardData.flags.total / executiveDashboardData.flags.total /
executiveDashboardData.users.total executiveDashboardData.users.total
).toFixed(1); ).toFixed(1);
}; }, [executiveDashboardData]);
const { gridTemplateColumns, chartSpan, userTrendsOrder, flagStatsOrder } =
useDashboardGrid();
return ( return (
<> <>
@ -42,14 +82,30 @@ export const ExecutiveDashboard: VFC = () => {
} }
/> />
</Box> </Box>
<StyledGrid> <StyledGrid sx={{ gridTemplateColumns }}>
<UserStats count={executiveDashboardData.users.total} /> <Widget title='Total users' order={1}>
<FlagStats <UserStats count={executiveDashboardData.users.total} />
count={executiveDashboardData.flags.total} </Widget>
flagsPerUser={calculateFlagPerUsers()} <Widget title='Users' order={userTrendsOrder} span={chartSpan}>
/> <UsersChart
<UsersChart userTrends={executiveDashboardData.userTrends} /> userTrends={executiveDashboardData.userTrends}
<FlagsChart flagTrends={executiveDashboardData.flagTrends} /> />
</Widget>
<Widget
title='Total flags'
tooltip='Total flags represent the total ctive flags (not archived) that currently exist across all projects of your application.'
order={flagStatsOrder}
>
<FlagStats
count={executiveDashboardData.flags.total}
flagsPerUser={flagPerUsers}
/>
</Widget>
<Widget title='Number of flags' order={4} span={chartSpan}>
<FlagsChart
flagTrends={executiveDashboardData.flagTrends}
/>
</Widget>
</StyledGrid> </StyledGrid>
</> </>
); );

View File

@ -1,6 +1,5 @@
import { Settings } from '@mui/icons-material'; import { Settings } from '@mui/icons-material';
import { Box, Typography, styled } from '@mui/material'; import { Box, Typography, styled } from '@mui/material';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
const StyledContent = styled(Box)(({ theme }) => ({ const StyledContent = styled(Box)(({ theme }) => ({
borderRadius: `${theme.shape.borderRadiusLarge}px`, borderRadius: `${theme.shape.borderRadiusLarge}px`,
@ -88,22 +87,7 @@ export const FlagStats: React.FC<IFlagStatsProps> = ({
flagsPerUser, flagsPerUser,
}) => { }) => {
return ( return (
<StyledContent> <>
<StyledHeader variant='h1'>
Total flags{' '}
<HelpIcon
htmlTooltip
tooltip={
<Box>
<Typography variant='body2'>
Total flags represent the total active flags
(not archived) that currently exist across all
projects of your application.
</Typography>
</Box>
}
/>
</StyledHeader>
<StyledRingContainer> <StyledRingContainer>
<StyledRing> <StyledRing>
<StyledRingContent>{count}</StyledRingContent> <StyledRingContent>{count}</StyledRingContent>
@ -126,6 +110,6 @@ export const FlagStats: React.FC<IFlagStatsProps> = ({
</StyledTextContainer> </StyledTextContainer>
<StyledFlagCountPerUser>{flagsPerUser}</StyledFlagCountPerUser> <StyledFlagCountPerUser>{flagsPerUser}</StyledFlagCountPerUser>
</StyledInsightsContainer> </StyledInsightsContainer>
</StyledContent> </>
); );
}; };

View File

@ -12,7 +12,7 @@ import {
} from 'chart.js'; } from 'chart.js';
import { Line } from 'react-chartjs-2'; import { Line } from 'react-chartjs-2';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import { Paper, Theme, Typography, useTheme } from '@mui/material'; import { Theme, useTheme } from '@mui/material';
import { import {
useLocationSettings, useLocationSettings,
type ILocationSettings, type ILocationSettings,
@ -116,17 +116,7 @@ const FlagsChartComponent: VFC<IFlagsChartComponentProps> = ({
); );
const options = createOptions(theme, locationSettings); const options = createOptions(theme, locationSettings);
return ( return <Line options={options} data={data} />;
<Paper sx={(theme) => ({ padding: theme.spacing(4) })}>
<Typography
variant='h3'
sx={(theme) => ({ marginBottom: theme.spacing(3) })}
>
Number of flags
</Typography>
<Line options={options} data={data} />
</Paper>
);
}; };
ChartJS.register( ChartJS.register(

View File

@ -5,13 +5,6 @@ import { useUiFlag } from 'hooks/useUiFlag';
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
const StyledContent = styled(Box)(({ theme }) => ({
borderRadius: `${theme.shape.borderRadiusLarge}px`,
backgroundColor: theme.palette.background.paper,
maxWidth: 300,
padding: theme.spacing(3),
}));
const StyledUserContainer = styled(Box)(({ theme }) => ({ const StyledUserContainer = styled(Box)(({ theme }) => ({
position: 'relative', position: 'relative',
})); }));
@ -85,49 +78,42 @@ export const UserStats: React.FC<IUserStatsProps> = ({ count }) => {
return ( return (
<> <>
<Box sx={{ display: 'flex', flexDirection: 'column' }}> <StyledUserContainer>
<StyledContent> <StyledUserBox>
<StyledHeader variant='h1'>Total users</StyledHeader> <StyledUserCount variant='h2'>{count}</StyledUserCount>
<StyledUserContainer> </StyledUserBox>
<StyledUserBox> <StyledCustomShadow />
<StyledUserCount variant='h2'> </StyledUserContainer>
{count}
</StyledUserCount>
</StyledUserBox>
<StyledCustomShadow />
</StyledUserContainer>
<ConditionallyRender <ConditionallyRender
condition={showInactiveUsers} condition={showInactiveUsers}
show={ show={
<> <>
<StyledUserDistributionContainer> <StyledUserDistributionContainer>
<UserDistribution /> <UserDistribution />
</StyledUserDistributionContainer> </StyledUserDistributionContainer>
<StyledDistInfoContainer> <StyledDistInfoContainer>
<UserDistributionInfo <UserDistributionInfo
type='active' type='active'
percentage='70' percentage='70'
count='9999' count='9999'
/> />
<UserDistributionInfo <UserDistributionInfo
type='inactive' type='inactive'
percentage='30' percentage='30'
count='9999' count='9999'
/> />
</StyledDistInfoContainer> </StyledDistInfoContainer>
</> </>
} }
/> />
<StyledLinkContainer> <StyledLinkContainer>
<StyledLink to='/admin/users'> <StyledLink to='/admin/users'>
View users <ChevronRight /> View users <ChevronRight />
</StyledLink> </StyledLink>
</StyledLinkContainer> </StyledLinkContainer>
</StyledContent>
</Box>
</> </>
); );
}; };

View File

@ -13,7 +13,7 @@ import {
} from 'chart.js'; } from 'chart.js';
import { Line } from 'react-chartjs-2'; import { Line } from 'react-chartjs-2';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import { Paper, Theme, Typography, useTheme } from '@mui/material'; import { Theme, useTheme } from '@mui/material';
import { import {
useLocationSettings, useLocationSettings,
type ILocationSettings, type ILocationSettings,
@ -181,23 +181,10 @@ const UsersChartComponent: VFC<IUsersChartComponentProps> = ({
); );
return ( return (
<Paper <>
elevation={0}
sx={(theme) => ({
padding: theme.spacing(3),
position: 'relative',
})}
>
<Typography
variant='h3'
sx={(theme) => ({ marginBottom: theme.spacing(3) })}
>
Users
</Typography>
<Line options={options} data={data} /> <Line options={options} data={data} />
<ChartTooltip tooltip={tooltip} /> <ChartTooltip tooltip={tooltip} />
</Paper> </>
); );
}; };

View File

@ -0,0 +1,39 @@
import { FC, ReactNode } from 'react';
import { Paper, Typography } from '@mui/material';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
export const Widget: FC<{
title: ReactNode;
order?: number;
span?: number;
tooltip?: ReactNode;
}> = ({ title, order, children, span = 1, tooltip }) => (
<Paper
elevation={0}
sx={(theme) => ({
padding: 3,
borderRadius: `${theme.shape.borderRadiusLarge}px`,
order,
gridColumn: `span ${span}`,
minWidth: 0, // bugfix, see: https://github.com/chartjs/Chart.js/issues/4156#issuecomment-295180128
})}
>
<Typography
variant='h3'
sx={(theme) => ({
marginBottom: theme.spacing(3),
display: 'flex',
alignItems: 'center',
gap: theme.spacing(0.5),
})}
>
{title}
<ConditionallyRender
condition={Boolean(tooltip)}
show={<HelpIcon htmlTooltip tooltip={tooltip} />}
/>
</Typography>
{children}
</Paper>
);