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:
parent
c7f13aec0b
commit
9ac1c88bd4
@ -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>
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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',
|
||||||
|
@ -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',
|
||||||
|
@ -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 */
|
||||||
|
Loading…
Reference in New Issue
Block a user