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:
parent
906edec1b6
commit
19121f234e
@ -53,7 +53,7 @@ const BreadcrumbNav = () => {
|
|||||||
)
|
)
|
||||||
.map(decodeURI);
|
.map(decodeURI);
|
||||||
|
|
||||||
if (paths.length === 0) {
|
if (location.pathname === '/insights') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 }) => ({
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @deprecated remove with insightsV2 flag
|
||||||
|
*/
|
||||||
export const chartInfo = {
|
export const chartInfo = {
|
||||||
totalUsers: {
|
totalUsers: {
|
||||||
title: 'Total users',
|
title: 'Total users',
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
);
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user