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:
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 { 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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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(
|
||||||
|
@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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