mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
feat: executive dashboard responsive grid (#6069)
- unified "Widget" component - column order dependent on screen width
This commit is contained in:
parent
ccc41dca4e
commit
c9ac4916e8
@ -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 { VFC } from 'react';
|
||||
import { UsersChart } from './UsersChart/UsersChart';
|
||||
import { FlagsChart } from './FlagsChart/FlagsChart';
|
||||
import { useExecutiveDashboard } from 'hooks/api/getters/useExecutiveSummary/useExecutiveSummary';
|
||||
import { UserStats } from './UserStats/UserStats';
|
||||
import { FlagStats } from './FlagStats/FlagStats';
|
||||
import { Widget } from './Widget/Widget';
|
||||
|
||||
const StyledGrid = styled(Box)(({ theme }) => ({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `300px 1fr`,
|
||||
// TODO: responsive grid size
|
||||
gridAutoRows: 'auto',
|
||||
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 = () => {
|
||||
const { executiveDashboardData, loading, error } = useExecutiveDashboard();
|
||||
|
||||
const calculateFlagPerUsers = () => {
|
||||
const flagPerUsers = useMemo(() => {
|
||||
if (
|
||||
executiveDashboardData.users.total === 0 ||
|
||||
executiveDashboardData.flags.total === 0
|
||||
@ -29,7 +66,10 @@ export const ExecutiveDashboard: VFC = () => {
|
||||
executiveDashboardData.flags.total /
|
||||
executiveDashboardData.users.total
|
||||
).toFixed(1);
|
||||
};
|
||||
}, [executiveDashboardData]);
|
||||
|
||||
const { gridTemplateColumns, chartSpan, userTrendsOrder, flagStatsOrder } =
|
||||
useDashboardGrid();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -42,14 +82,30 @@ export const ExecutiveDashboard: VFC = () => {
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<StyledGrid>
|
||||
<UserStats count={executiveDashboardData.users.total} />
|
||||
<FlagStats
|
||||
count={executiveDashboardData.flags.total}
|
||||
flagsPerUser={calculateFlagPerUsers()}
|
||||
/>
|
||||
<UsersChart userTrends={executiveDashboardData.userTrends} />
|
||||
<FlagsChart flagTrends={executiveDashboardData.flagTrends} />
|
||||
<StyledGrid sx={{ gridTemplateColumns }}>
|
||||
<Widget title='Total users' order={1}>
|
||||
<UserStats count={executiveDashboardData.users.total} />
|
||||
</Widget>
|
||||
<Widget title='Users' order={userTrendsOrder} span={chartSpan}>
|
||||
<UsersChart
|
||||
userTrends={executiveDashboardData.userTrends}
|
||||
/>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Settings } from '@mui/icons-material';
|
||||
import { Box, Typography, styled } from '@mui/material';
|
||||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||
|
||||
const StyledContent = styled(Box)(({ theme }) => ({
|
||||
borderRadius: `${theme.shape.borderRadiusLarge}px`,
|
||||
@ -88,22 +87,7 @@ export const FlagStats: React.FC<IFlagStatsProps> = ({
|
||||
flagsPerUser,
|
||||
}) => {
|
||||
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>
|
||||
<StyledRing>
|
||||
<StyledRingContent>{count}</StyledRingContent>
|
||||
@ -126,6 +110,6 @@ export const FlagStats: React.FC<IFlagStatsProps> = ({
|
||||
</StyledTextContainer>
|
||||
<StyledFlagCountPerUser>{flagsPerUser}</StyledFlagCountPerUser>
|
||||
</StyledInsightsContainer>
|
||||
</StyledContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
} from 'chart.js';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { Paper, Theme, Typography, useTheme } from '@mui/material';
|
||||
import { Theme, useTheme } from '@mui/material';
|
||||
import {
|
||||
useLocationSettings,
|
||||
type ILocationSettings,
|
||||
@ -116,17 +116,7 @@ const FlagsChartComponent: VFC<IFlagsChartComponentProps> = ({
|
||||
);
|
||||
const options = createOptions(theme, locationSettings);
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
return <Line options={options} data={data} />;
|
||||
};
|
||||
|
||||
ChartJS.register(
|
||||
|
@ -5,13 +5,6 @@ import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import React from 'react';
|
||||
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',
|
||||
}));
|
||||
@ -85,49 +78,42 @@ export const UserStats: React.FC<IUserStatsProps> = ({ count }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<StyledContent>
|
||||
<StyledHeader variant='h1'>Total users</StyledHeader>
|
||||
<StyledUserContainer>
|
||||
<StyledUserBox>
|
||||
<StyledUserCount variant='h2'>
|
||||
{count}
|
||||
</StyledUserCount>
|
||||
</StyledUserBox>
|
||||
<StyledCustomShadow />
|
||||
</StyledUserContainer>
|
||||
<StyledUserContainer>
|
||||
<StyledUserBox>
|
||||
<StyledUserCount variant='h2'>{count}</StyledUserCount>
|
||||
</StyledUserBox>
|
||||
<StyledCustomShadow />
|
||||
</StyledUserContainer>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={showInactiveUsers}
|
||||
show={
|
||||
<>
|
||||
<StyledUserDistributionContainer>
|
||||
<UserDistribution />
|
||||
</StyledUserDistributionContainer>
|
||||
<ConditionallyRender
|
||||
condition={showInactiveUsers}
|
||||
show={
|
||||
<>
|
||||
<StyledUserDistributionContainer>
|
||||
<UserDistribution />
|
||||
</StyledUserDistributionContainer>
|
||||
|
||||
<StyledDistInfoContainer>
|
||||
<UserDistributionInfo
|
||||
type='active'
|
||||
percentage='70'
|
||||
count='9999'
|
||||
/>
|
||||
<UserDistributionInfo
|
||||
type='inactive'
|
||||
percentage='30'
|
||||
count='9999'
|
||||
/>
|
||||
</StyledDistInfoContainer>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
<StyledLinkContainer>
|
||||
<StyledLink to='/admin/users'>
|
||||
View users <ChevronRight />
|
||||
</StyledLink>
|
||||
</StyledLinkContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
} from 'chart.js';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { Paper, Theme, Typography, useTheme } from '@mui/material';
|
||||
import { Theme, useTheme } from '@mui/material';
|
||||
import {
|
||||
useLocationSettings,
|
||||
type ILocationSettings,
|
||||
@ -181,23 +181,10 @@ const UsersChartComponent: VFC<IUsersChartComponentProps> = ({
|
||||
);
|
||||
|
||||
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} />
|
||||
|
||||
<ChartTooltip tooltip={tooltip} />
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
39
frontend/src/component/executiveDashboard/Widget/Widget.tsx
Normal file
39
frontend/src/component/executiveDashboard/Widget/Widget.tsx
Normal 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>
|
||||
);
|
Loading…
Reference in New Issue
Block a user