1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: new user widget (#6037)

Preliminary code for executive dashboard user widget
This commit is contained in:
Fredrik Strand Oseberg 2024-01-25 14:43:59 +01:00 committed by GitHub
parent c7f13aec0b
commit 9ac1c88bd4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 233 additions and 3 deletions

View File

@ -3,6 +3,7 @@ import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { VFC } from 'react'; 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 { UserStats } from './UserStats/UserStats';
const StyledGrid = styled(Box)(({ theme }) => ({ const StyledGrid = styled(Box)(({ theme }) => ({
display: 'grid', display: 'grid',
@ -26,7 +27,7 @@ export const ExecutiveDashboard: VFC = () => {
</Box> </Box>
{/* Dashboard */} {/* Dashboard */}
<StyledGrid> <StyledGrid>
<Paper>Stats</Paper> <UserStats />
<UsersChart /> <UsersChart />
<FlagsChart /> <FlagsChart />
</StyledGrid> </StyledGrid>

View File

@ -0,0 +1,223 @@
import { ChevronRight } from '@mui/icons-material';
import { Box, Typography, styled } from '@mui/material';
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 }) => ({
position: 'relative',
}));
const StyledUserBox = styled(Box)(({ theme }) => ({
borderRadius: `${theme.shape.borderRadiusExtraLarge}px`,
backgroundColor: theme.palette.primary.main,
maxWidth: 300,
padding: theme.spacing(2),
marginBottom: theme.spacing(3),
position: 'relative',
zIndex: 2,
}));
const StyledCustomShadow = styled(Box)(({ theme }) => ({
width: '220px',
height: '54px',
backgroundColor: 'rgba(108, 101, 229, 0.30)',
position: 'absolute',
margin: '0 auto',
top: '45px',
left: '15px',
borderRadius: `${theme.shape.borderRadiusExtraLarge}px`,
zIndex: 1,
}));
const StyledUserDistributionContainer = styled(Box)(({ theme }) => ({
marginBottom: theme.spacing(2.5),
}));
const StyledUserCount = styled(Typography)(({ theme }) => ({
color: theme.palette.primary.contrastText,
fontWeight: 'bold',
fontSize: theme.fontSizes.extraLargeHeader,
margin: 0,
padding: 0,
}));
const StyledHeader = styled(Typography)(({ theme }) => ({
marginBottom: theme.spacing(3),
fontSize: theme.fontSizes.bodySize,
fontWeight: 'bold',
}));
const StyledDistInfoContainer = styled(Box)({
display: 'flex',
flexDirection: 'column',
gap: '12px',
});
const StyledLinkContainer = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
marginTop: theme.spacing(3),
}));
const StyledLink = styled(Link)({
fontWeight: 'bold',
textDecoration: 'none',
display: 'flex',
justifyContent: 'center',
});
export const UserStats = () => {
return (
<>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<StyledContent>
<StyledHeader variant='h1'>Total users</StyledHeader>
<StyledUserContainer>
<StyledUserBox>
<StyledUserCount variant='h2'>9999</StyledUserCount>
</StyledUserBox>
<StyledCustomShadow />
</StyledUserContainer>
<StyledUserDistributionContainer>
<UserDistribution />
</StyledUserDistributionContainer>
<StyledDistInfoContainer>
<UserDistributionInfo
type='active'
percentage='70'
count='9999'
/>
<UserDistributionInfo
type='inactive'
percentage='30'
count='9999'
/>
</StyledDistInfoContainer>
<StyledLinkContainer>
<StyledLink to='/admin/users'>
View users <ChevronRight />
</StyledLink>
</StyledLinkContainer>
</StyledContent>
</Box>
</>
);
};
type UserType = 'active' | 'inactive';
interface StyledLinearProgressProps {
type: UserType;
}
const StyledUserDistributionLine = styled(Box)<StyledLinearProgressProps>(
({ theme, type }) => ({
borderRadius: theme.shape.borderRadius,
height: 16,
backgroundColor:
type === 'active'
? theme.palette.success.border
: theme.palette.warning.border,
}),
);
const UserDistribution = () => {
const getLineWidth = () => {
return [80, 20];
};
const [activeWidth, inactiveWidth] = getLineWidth();
return (
<Box sx={{ display: 'flex' }}>
<StyledUserDistributionLine
type='active'
sx={{ width: `${activeWidth}%` }}
/>
<StyledUserDistributionLine
type='inactive'
sx={(theme) => ({
width: `${inactiveWidth}%`,
marginLeft: theme.spacing(0.5),
})}
/>
</Box>
);
};
const StyledUserDistContainer = styled(Box)(({ theme }) => ({
padding: `${theme.spacing(1.5)} ${theme.spacing(2)}`,
borderRadius: `${theme.shape.borderRadius}px`,
border: `1px solid ${theme.palette.divider}`,
}));
const StyledUserDistIndicator = styled(Box)<StyledLinearProgressProps>(
({ theme, type }) => ({
width: 8,
height: 8,
backgroundColor:
type === 'active'
? theme.palette.success.border
: theme.palette.warning.border,
borderRadius: `2px`,
marginRight: theme.spacing(1),
marginTop: theme.spacing(0.8),
}),
);
interface IUserDistributionInfoProps {
type: UserType;
count: string;
percentage: string;
}
const StyledDistInfoInnerContainer = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
width: '100%',
}));
const StyledDistInfoTextContainer = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
}));
const StyledCountTypography = styled(Typography)(({ theme }) => ({
marginLeft: 'auto',
fontWeight: 'normal',
}));
const UserDistributionInfo: React.FC<IUserDistributionInfoProps> = ({
type,
count,
percentage,
}) => {
return (
<StyledUserDistContainer>
<Box sx={{ display: 'flex' }}>
<StyledUserDistIndicator type={type} />
<StyledDistInfoInnerContainer>
<StyledDistInfoTextContainer>
<Typography variant='body1'>
{type === 'active' ? 'Active' : 'Inactive'} users
</Typography>
<Typography variant='body2'>{percentage}%</Typography>
</StyledDistInfoTextContainer>
<StyledCountTypography variant='h2'>
{count}
</StyledCountTypography>
</StyledDistInfoInnerContainer>
</Box>
</StyledUserDistContainer>
);
};

View File

@ -56,6 +56,7 @@ const theme = {
}, },
}, },
fontSizes: { fontSizes: {
extraLargeHeader: '2.5rem',
largeHeader: '2rem', largeHeader: '2rem',
mainHeader: '1.25rem', mainHeader: '1.25rem',
bodySize: '1rem', bodySize: '1rem',

View File

@ -48,6 +48,7 @@ export const theme = {
}, },
}, },
fontSizes: { fontSizes: {
extraLargeHeader: '2.5rem',
largeHeader: '2rem', largeHeader: '2rem',
mainHeader: '1.25rem', mainHeader: '1.25rem',
bodySize: '1rem', bodySize: '1rem',

View File

@ -7,6 +7,7 @@ declare module '@mui/material/styles' {
* @deprecated * @deprecated
*/ */
fontSizes: { fontSizes: {
extraLargeHeader: string;
largeHeader: string; largeHeader: string;
mainHeader: string; mainHeader: string;
bodySize: string; bodySize: string;
@ -121,13 +122,16 @@ declare module '@mui/material/styles' {
**/ **/
variants: string[]; variants: string[];
} }
// biome-ignore lint/suspicious/noEmptyInterface: We need this to keep types from breaking
interface Theme extends CustomTheme {} interface Theme extends CustomTheme {}
// biome-ignore lint/suspicious/noEmptyInterface: We need this to keep types from breaking
interface ThemeOptions extends CustomTheme {} interface ThemeOptions extends CustomTheme {}
// biome-ignore lint/suspicious/noEmptyInterface: We need this to keep types from breaking
interface Palette extends CustomPalette {} interface Palette extends CustomPalette {}
// biome-ignore lint/suspicious/noEmptyInterface: We need this to keep types from breaking
interface PaletteOptions extends CustomPalette {} interface PaletteOptions extends CustomPalette {}
// biome-ignore lint/suspicious/noEmptyInterface: We need this to keep types from breaking
interface TypeBackground extends CustomTypeBackground {} interface TypeBackground extends CustomTypeBackground {}
/* Extend the background object from MUI */ /* Extend the background object from MUI */