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>
<StyledWidgetStats>
<WidgetTitle title='Total users' />
<UserStats <UserStats
count={users.total} count={users.total}
active={users.active} active={users.active}
inactive={users.inactive} inactive={users.inactive}
isLoading={loading} isLoading={loading}
/> />
</Widget> </StyledWidgetStats>
} <StyledChartContainer>
elseShow={
<Widget
{...(isOneProjectSelected
? chartInfo.usersInProject
: chartInfo.avgUsersPerProject)}
>
<UserStats
count={summary.averageUsers}
isLoading={loading}
/>
</Widget>
}
/>
<ConditionallyRender
condition={showAllProjects}
show={
<ChartWidget {...chartInfo.users}>
<UsersChart <UsersChart
userTrends={userTrends} userTrends={userTrends}
isLoading={loading} isLoading={loading}
/> />
</ChartWidget> </StyledChartContainer>
} </StyledWidget>
elseShow={ <StyledWidget>
<ChartWidget {...chartInfo.usersPerProject}> <StyledWidgetStats width={275}>
<UsersPerProjectChart <WidgetTitle title='Flags' />
projectFlagTrends={groupedProjectsData}
isLoading={loading}
/>
</ChartWidget>
}
/>
<Widget {...chartInfo.totalFlags}>
<FlagStats <FlagStats
count={showAllProjects ? flags.total : summary.total} count={flags.total}
flagsPerUser={ flagsPerUser={getFlagsPerUser(flags, users)}
showAllProjects ? getFlagsPerUser(flags, users) : ''
}
isLoading={loading} isLoading={loading}
/> />
</Widget> </StyledWidgetStats>
<ConditionallyRender <StyledChartContainer>
condition={showAllProjects}
show={
<ChartWidget {...chartInfo.flags}>
<FlagsChart <FlagsChart
flagTrends={flagTrends} flagTrends={flagTrends}
isLoading={loading} isLoading={loading}
/> />
</ChartWidget> </StyledChartContainer>
</StyledWidget>
</>
} }
elseShow={ elseShow={
<ChartWidget {...chartInfo.flagsPerProject}> <>
<StyledWidget>
<StyledWidgetStats>
<WidgetTitle
title={
isOneProjectSelected
? 'Users in project'
: 'Users per project on average'
}
tooltip={
isOneProjectSelected
? 'Number of users in selected projects.'
: 'Average number of users for selected projects.'
}
/>
<UserStats
count={summary.averageUsers}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<UsersPerProjectChart
projectFlagTrends={groupedProjectsData}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
<StyledWidget>
<StyledWidgetStats width={275}>
<WidgetTitle title='Flags' />
<FlagStats
count={summary.total}
flagsPerUser={''}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<FlagsProjectChart <FlagsProjectChart
projectFlagTrends={groupedProjectsData} projectFlagTrends={groupedProjectsData}
isLoading={loading} isLoading={loading}
/> />
</ChartWidget> </StyledChartContainer>
</StyledWidget>
</>
} }
/> />
<ConditionallyRender <ConditionallyRender
condition={isEnterprise()} condition={isEnterprise()}
show={ show={
<> <>
<Widget {...chartInfo.averageHealth}> <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>
<WidgetTitle
title='Flag evaluation metrics'
tooltip='Summary of all flag evaluations reported by SDKs.'
/> />
</StyledGrid> <StyledChartContainer>
<ConditionallyRender
condition={isEnterprise()}
show={
<>
<Widget
{...(showAllProjects
? chartInfo.metrics
: chartInfo.metricsPerProject)}
>
<MetricsSummaryChart <MetricsSummaryChart
metricsSummaryTrends={groupedMetricsData} metricsSummaryTrends={
allDatapointsSorted={allMetricsDatapoints} groupedMetricsData
}
allDatapointsSorted={
allMetricsDatapoints
}
isAggregate={showAllProjects} isAggregate={showAllProjects}
isLoading={loading} isLoading={loading}
/> />
</Widget> </StyledChartContainer>
<Widget </StyledWidgetContent>
{...chartInfo.updates} </StyledWidget>
sx={{ mt: (theme) => theme.spacing(2) }} <StyledWidget>
> <StyledWidgetContent>
<WidgetTitle
title='Updates per environment type'
tooltip='Summary of all configuration updates per environment type.'
/>
<UpdatesPerEnvironmentTypeChart <UpdatesPerEnvironmentTypeChart
environmentTypeTrends={environmentTypeTrends} environmentTypeTrends={
environmentTypeTrends
}
isLoading={loading} isLoading={loading}
/> />
</Widget> </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>
); );
}; };