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

Insights layout (#7610)

Refactored insights page - stats and charts relevant to the same metric
are now combined into a single widget.
This commit is contained in:
Tymoteusz Czech 2024-07-18 12:43:52 +02:00 committed by GitHub
parent 906edec1b6
commit 19121f234e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 232 additions and 145 deletions

View File

@ -53,7 +53,7 @@ const BreadcrumbNav = () => {
) )
.map(decodeURI); .map(decodeURI);
if (paths.length === 0) { if (location.pathname === '/insights') {
return null; return null;
} }

View File

@ -17,7 +17,7 @@ import { InsightsFilters } from './InsightsFilters';
import { FilterItemParam } from '../../utils/serializeQueryParams'; import { FilterItemParam } from '../../utils/serializeQueryParams';
const StyledWrapper = styled('div')(({ theme }) => ({ const StyledWrapper = styled('div')(({ theme }) => ({
paddingTop: theme.spacing(1), paddingTop: theme.spacing(2),
})); }));
const StickyContainer = styled(Sticky)(({ theme }) => ({ const StickyContainer = styled(Sticky)(({ theme }) => ({

View File

@ -1,7 +1,5 @@
import type { FC } from 'react'; import type { FC } from 'react';
import { Box, styled } from '@mui/material'; import { Box, Paper, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Widget } from './components/Widget/Widget';
import { UserStats } from './componentsStat/UserStats/UserStats'; import { UserStats } from './componentsStat/UserStats/UserStats';
import { UsersChart } from './componentsChart/UsersChart/UsersChart'; import { UsersChart } from './componentsChart/UsersChart/UsersChart';
import { UsersPerProjectChart } from './componentsChart/UsersPerProjectChart/UsersPerProjectChart'; import { UsersPerProjectChart } from './componentsChart/UsersPerProjectChart/UsersPerProjectChart';
@ -21,8 +19,9 @@ import type {
} from 'openapi'; } from 'openapi';
import type { GroupedDataByProject } from './hooks/useGroupedProjectTrends'; import type { GroupedDataByProject } from './hooks/useGroupedProjectTrends';
import { allOption } from 'component/common/ProjectSelect/ProjectSelect'; import { allOption } from 'component/common/ProjectSelect/ProjectSelect';
import { chartInfo } from './chart-info';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { WidgetTitle } from './components/WidgetTitle/WidgetTitle';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
export interface IChartsProps { export interface IChartsProps {
flags: InstanceInsightsSchema['flags']; flags: InstanceInsightsSchema['flags'];
@ -53,22 +52,48 @@ export interface IChartsProps {
allMetricsDatapoints: string[]; allMetricsDatapoints: string[];
} }
const StyledGrid = styled(Box)(({ theme }) => ({ const StyledContainer = styled(Box)(({ theme }) => ({
display: 'grid', display: 'flex',
gridTemplateColumns: `repeat(2, 1fr)`, flexDirection: 'column',
gridAutoRows: 'auto',
gap: theme.spacing(2), gap: theme.spacing(2),
paddingBottom: theme.spacing(2), }));
const StyledWidget = styled(Paper)(({ theme }) => ({
borderRadius: `${theme.shape.borderRadiusLarge}px`,
boxShadow: 'none',
display: 'flex',
flexWrap: 'wrap',
[theme.breakpoints.up('md')]: { [theme.breakpoints.up('md')]: {
gridTemplateColumns: `300px 1fr`, flexDirection: 'row',
flexWrap: 'nowrap',
}, },
})); }));
const ChartWidget = styled(Widget)(({ theme }) => ({ const StyledWidgetContent = styled(Box)(({ theme }) => ({
[theme.breakpoints.down('md')]: { padding: theme.spacing(3),
gridColumnStart: 'span 2', width: '100%',
order: 2, }));
},
const StyledWidgetStats = styled(Box)<{ width?: number }>(
({ theme, width = 300 }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
padding: theme.spacing(3),
minWidth: '100%',
[theme.breakpoints.up('md')]: {
minWidth: `${width}px`,
borderRight: `1px solid ${theme.palette.background.application}`,
},
}),
);
const StyledChartContainer = styled(Box)(({ theme }) => ({
position: 'relative',
minWidth: 0, // bugfix, see: https://github.com/chartjs/Chart.js/issues/4156#issuecomment-295180128
flexGrow: 1,
margin: 'auto 0',
padding: theme.spacing(3),
})); }));
export const InsightsCharts: FC<IChartsProps> = ({ export const InsightsCharts: FC<IChartsProps> = ({
@ -84,9 +109,9 @@ export const InsightsCharts: FC<IChartsProps> = ({
allMetricsDatapoints, allMetricsDatapoints,
loading, loading,
}) => { }) => {
const { isEnterprise } = useUiConfig();
const showAllProjects = projects[0] === allOption.id; const showAllProjects = projects[0] === allOption.id;
const isOneProjectSelected = projects.length === 1; const isOneProjectSelected = projects.length === 1;
const { isEnterprise } = useUiConfig();
function getFlagsPerUser( function getFlagsPerUser(
flags: InstanceInsightsSchemaFlags, flags: InstanceInsightsSchemaFlags,
@ -99,153 +124,172 @@ export const InsightsCharts: FC<IChartsProps> = ({
} }
return ( return (
<> <StyledContainer>
<StyledGrid> <ConditionallyRender
<ConditionallyRender condition={showAllProjects}
condition={showAllProjects} show={
show={ <>
<Widget {...chartInfo.totalUsers}> <StyledWidget>
<UserStats <StyledWidgetStats>
count={users.total} <WidgetTitle title='Total users' />
active={users.active} <UserStats
inactive={users.inactive} count={users.total}
isLoading={loading} active={users.active}
/> inactive={users.inactive}
</Widget> isLoading={loading}
} />
elseShow={ </StyledWidgetStats>
<Widget <StyledChartContainer>
{...(isOneProjectSelected <UsersChart
? chartInfo.usersInProject userTrends={userTrends}
: chartInfo.avgUsersPerProject)} isLoading={loading}
> />
<UserStats </StyledChartContainer>
count={summary.averageUsers} </StyledWidget>
isLoading={loading} <StyledWidget>
/> <StyledWidgetStats width={275}>
</Widget> <WidgetTitle title='Flags' />
} <FlagStats
/> count={flags.total}
<ConditionallyRender flagsPerUser={getFlagsPerUser(flags, users)}
condition={showAllProjects} isLoading={loading}
show={ />
<ChartWidget {...chartInfo.users}> </StyledWidgetStats>
<UsersChart <StyledChartContainer>
userTrends={userTrends} <FlagsChart
isLoading={loading} flagTrends={flagTrends}
/> isLoading={loading}
</ChartWidget> />
} </StyledChartContainer>
elseShow={ </StyledWidget>
<ChartWidget {...chartInfo.usersPerProject}> </>
<UsersPerProjectChart }
projectFlagTrends={groupedProjectsData} elseShow={
isLoading={loading} <>
/> <StyledWidget>
</ChartWidget> <StyledWidgetStats>
} <WidgetTitle
/> title={
<Widget {...chartInfo.totalFlags}> isOneProjectSelected
<FlagStats ? 'Users in project'
count={showAllProjects ? flags.total : summary.total} : 'Users per project on average'
flagsPerUser={ }
showAllProjects ? getFlagsPerUser(flags, users) : '' tooltip={
} isOneProjectSelected
isLoading={loading} ? 'Number of users in selected projects.'
/> : 'Average number of users for selected projects.'
</Widget> }
<ConditionallyRender />
condition={showAllProjects} <UserStats
show={ count={summary.averageUsers}
<ChartWidget {...chartInfo.flags}> isLoading={loading}
<FlagsChart />
flagTrends={flagTrends} </StyledWidgetStats>
isLoading={loading} <StyledChartContainer>
/> <UsersPerProjectChart
</ChartWidget> projectFlagTrends={groupedProjectsData}
} isLoading={loading}
elseShow={ />
<ChartWidget {...chartInfo.flagsPerProject}> </StyledChartContainer>
<FlagsProjectChart </StyledWidget>
projectFlagTrends={groupedProjectsData} <StyledWidget>
isLoading={loading} <StyledWidgetStats width={275}>
/> <WidgetTitle title='Flags' />
</ChartWidget> <FlagStats
} count={summary.total}
/> flagsPerUser={''}
<ConditionallyRender isLoading={loading}
condition={isEnterprise()} />
show={ </StyledWidgetStats>
<> <StyledChartContainer>
<Widget {...chartInfo.averageHealth}> <FlagsProjectChart
projectFlagTrends={groupedProjectsData}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
</>
}
/>
<ConditionallyRender
condition={isEnterprise()}
show={
<>
<StyledWidget>
<StyledWidgetStats width={288}>
<WidgetTitle title='Health' />
<HealthStats <HealthStats
value={summary.averageHealth} value={summary.averageHealth}
healthy={summary.active} healthy={summary.active}
stale={summary.stale} stale={summary.stale}
potentiallyStale={summary.potentiallyStale} potentiallyStale={summary.potentiallyStale}
/> />
</Widget> </StyledWidgetStats>
<ChartWidget <StyledChartContainer>
{...(showAllProjects
? chartInfo.overallHealth
: chartInfo.healthPerProject)}
>
<ProjectHealthChart <ProjectHealthChart
projectFlagTrends={groupedProjectsData} projectFlagTrends={groupedProjectsData}
isAggregate={showAllProjects} isAggregate={showAllProjects}
isLoading={loading} isLoading={loading}
/> />
</ChartWidget> </StyledChartContainer>
<Widget {...chartInfo.medianTimeToProduction}> </StyledWidget>
<StyledWidget>
<StyledWidgetStats>
<WidgetTitle
title='Median time to production'
tooltip={`How long does it currently take on average from when a feature flag was created until it was enabled in a "production" type environment. This is calculated only from feature flags of the type "release" and is the median across the selected projects.`}
/>
<TimeToProduction <TimeToProduction
daysToProduction={ daysToProduction={
summary.medianTimeToProduction summary.medianTimeToProduction
} }
/> />
</Widget> </StyledWidgetStats>
<ChartWidget <StyledChartContainer>
{...(showAllProjects
? chartInfo.timeToProduction
: chartInfo.timeToProductionPerProject)}
>
<TimeToProductionChart <TimeToProductionChart
projectFlagTrends={groupedProjectsData} projectFlagTrends={groupedProjectsData}
isAggregate={showAllProjects} isAggregate={showAllProjects}
isLoading={loading} isLoading={loading}
/> />
</ChartWidget> </StyledChartContainer>
</> </StyledWidget>
} <StyledWidget>
/> <StyledWidgetContent>
</StyledGrid> <WidgetTitle
<ConditionallyRender title='Flag evaluation metrics'
condition={isEnterprise()} tooltip='Summary of all flag evaluations reported by SDKs.'
show={ />
<> <StyledChartContainer>
<Widget <MetricsSummaryChart
{...(showAllProjects metricsSummaryTrends={
? chartInfo.metrics groupedMetricsData
: chartInfo.metricsPerProject)} }
> allDatapointsSorted={
<MetricsSummaryChart allMetricsDatapoints
metricsSummaryTrends={groupedMetricsData} }
allDatapointsSorted={allMetricsDatapoints} isAggregate={showAllProjects}
isAggregate={showAllProjects} isLoading={loading}
isLoading={loading} />
/> </StyledChartContainer>
</Widget> </StyledWidgetContent>
<Widget </StyledWidget>
{...chartInfo.updates} <StyledWidget>
sx={{ mt: (theme) => theme.spacing(2) }} <StyledWidgetContent>
> <WidgetTitle
<UpdatesPerEnvironmentTypeChart title='Updates per environment type'
environmentTypeTrends={environmentTypeTrends} tooltip='Summary of all configuration updates per environment type.'
isLoading={loading} />
/> <UpdatesPerEnvironmentTypeChart
</Widget> environmentTypeTrends={
environmentTypeTrends
}
isLoading={loading}
/>
</StyledWidgetContent>
</StyledWidget>
</> </>
} }
/> />
</> </StyledContainer>
); );
}; };

View File

@ -71,6 +71,9 @@ const ChartWidget = styled(Widget)(({ theme }) => ({
}, },
})); }));
/**
* @deprecated remove with insightsV2 flag
*/
export const LegacyInsightsCharts: VFC<IChartsProps> = ({ export const LegacyInsightsCharts: VFC<IChartsProps> = ({
projects, projects,
flags, flags,

View File

@ -1,3 +1,6 @@
/**
* @deprecated remove with insightsV2 flag
*/
export const chartInfo = { export const chartInfo = {
totalUsers: { totalUsers: {
title: 'Total users', title: 'Total users',

View File

@ -13,6 +13,9 @@ const StyledPaper = styled(Paper)(({ theme }) => ({
position: 'relative', position: 'relative',
})); }));
/**
* @deprecated remove with insightsV2 flag
*/
export const Widget: FC<{ export const Widget: FC<{
title: ReactNode; title: ReactNode;
tooltip?: ReactNode; tooltip?: ReactNode;

View File

@ -0,0 +1,29 @@
import type { FC, ReactNode } from 'react';
import { Typography } from '@mui/material';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import InfoOutlined from '@mui/icons-material/InfoOutlined';
export const WidgetTitle: FC<{
title: ReactNode;
tooltip?: ReactNode;
}> = ({ title, tooltip }) => (
<Typography
variant='h3'
sx={(theme) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(0.5),
})}
>
{title}
<ConditionallyRender
condition={Boolean(tooltip)}
show={
<HelpIcon htmlTooltip tooltip={tooltip}>
<InfoOutlined />
</HelpIcon>
}
/>
</Typography>
);

View File

@ -1,6 +1,6 @@
import type { FC } from 'react'; import type { FC } from 'react';
import { useThemeMode } from 'hooks/useThemeMode'; import { useThemeMode } from 'hooks/useThemeMode';
import { useTheme } from '@mui/material'; import { styled, useTheme } from '@mui/material';
interface IHealthStatsProps { interface IHealthStatsProps {
value?: string | number; value?: string | number;
@ -9,6 +9,11 @@ interface IHealthStatsProps {
potentiallyStale: number; potentiallyStale: number;
} }
const StyledSvg = styled('svg')(() => ({
maxWidth: '250px',
margin: '0 auto',
}));
export const HealthStats: FC<IHealthStatsProps> = ({ export const HealthStats: FC<IHealthStatsProps> = ({
value, value,
healthy, healthy,
@ -20,7 +25,7 @@ export const HealthStats: FC<IHealthStatsProps> = ({
const theme = useTheme(); const theme = useTheme();
return ( return (
<svg <StyledSvg
viewBox='0 0 268 281' viewBox='0 0 268 281'
fill='none' fill='none'
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
@ -307,6 +312,6 @@ export const HealthStats: FC<IHealthStatsProps> = ({
/> />
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </StyledSvg>
); );
}; };