mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-02 01:17:58 +02:00
Insights UI (#6341)
- style for headers in Insights dashboard and project selector - fixed React element key issue in gauge chart - fixed React attribute issue in Health stats - active/inactive user stats from backend
This commit is contained in:
parent
f351ad821b
commit
fd87fd4e7d
@ -24,6 +24,7 @@ import {
|
|||||||
ExecutiveSummarySchemaProjectFlagTrendsItem,
|
ExecutiveSummarySchemaProjectFlagTrendsItem,
|
||||||
} from '../../openapi';
|
} from '../../openapi';
|
||||||
import { HealthStats } from './HealthStats/HealthStats';
|
import { HealthStats } from './HealthStats/HealthStats';
|
||||||
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
|
||||||
const StyledGrid = styled(Box)(({ theme }) => ({
|
const StyledGrid = styled(Box)(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@ -128,15 +129,28 @@ export const ExecutiveDashboard: VFC = () => {
|
|||||||
<Box sx={(theme) => ({ paddingBottom: theme.spacing(4) })}>
|
<Box sx={(theme) => ({ paddingBottom: theme.spacing(4) })}>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
titleElement={
|
titleElement={
|
||||||
<Typography variant='h1' component='span'>
|
<Typography
|
||||||
Dashboard
|
variant='h1'
|
||||||
|
component='div'
|
||||||
|
sx={(theme) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span>Insights</span>{' '}
|
||||||
|
<Badge color='warning'>Beta</Badge>
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<StyledGrid sx={{ gridTemplateColumns }}>
|
<StyledGrid sx={{ gridTemplateColumns }}>
|
||||||
<Widget title='Total users' order={1}>
|
<Widget title='Total users' order={1}>
|
||||||
<UserStats count={executiveDashboardData.users.total} />
|
<UserStats
|
||||||
|
count={executiveDashboardData.users.total}
|
||||||
|
active={executiveDashboardData.users.active}
|
||||||
|
inactive={executiveDashboardData.users.inactive}
|
||||||
|
/>
|
||||||
</Widget>
|
</Widget>
|
||||||
<Widget title='Users' order={userTrendsOrder} span={chartSpan}>
|
<Widget title='Users' order={userTrendsOrder} span={chartSpan}>
|
||||||
<UsersChart
|
<UsersChart
|
||||||
@ -146,7 +160,7 @@ export const ExecutiveDashboard: VFC = () => {
|
|||||||
</Widget>
|
</Widget>
|
||||||
<Widget
|
<Widget
|
||||||
title='Total flags'
|
title='Total flags'
|
||||||
tooltip='Total flags represent the total ctive flags (not archived) that currently exist across all projects of your application.'
|
tooltip='Total flags represent the total active flags (not archived) that currently exist across all projects of your application.'
|
||||||
order={flagStatsOrder}
|
order={flagStatsOrder}
|
||||||
>
|
>
|
||||||
<FlagStats
|
<FlagStats
|
||||||
@ -175,10 +189,10 @@ export const ExecutiveDashboard: VFC = () => {
|
|||||||
<Widget title='Average health' order={6}>
|
<Widget title='Average health' order={6}>
|
||||||
<HealthStats
|
<HealthStats
|
||||||
// FIXME: data from API
|
// FIXME: data from API
|
||||||
value={90}
|
value={80}
|
||||||
healthy={50}
|
healthy={4}
|
||||||
stale={10}
|
stale={1}
|
||||||
potenciallyStale={5}
|
potenciallyStale={0}
|
||||||
/>
|
/>
|
||||||
</Widget>
|
</Widget>
|
||||||
<Widget title='Health per project' order={7} span={chartSpan}>
|
<Widget title='Health per project' order={7} span={chartSpan}>
|
||||||
@ -199,7 +213,7 @@ export const ExecutiveDashboard: VFC = () => {
|
|||||||
<Widget title='Average time to production' order={9}>
|
<Widget title='Average time to production' order={9}>
|
||||||
<TimeToProduction
|
<TimeToProduction
|
||||||
//FIXME: data from API
|
//FIXME: data from API
|
||||||
daysToProduction={12}
|
daysToProduction={5.2}
|
||||||
/>
|
/>
|
||||||
</Widget>
|
</Widget>
|
||||||
<Widget title='Time to production' order={10} span={chartSpan}>
|
<Widget title='Time to production' order={10} span={chartSpan}>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { VFC } from 'react';
|
import { Fragment, VFC } from 'react';
|
||||||
import { Box, useTheme } from '@mui/material';
|
import { Box, useTheme } from '@mui/material';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
@ -55,9 +55,8 @@ const GaugeLines = () => {
|
|||||||
const end = polarToCartesian(0, 0, endRadius, angle);
|
const end = polarToCartesian(0, 0, endRadius, angle);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment key={angle}>
|
||||||
<path
|
<path
|
||||||
key={angle}
|
|
||||||
d={`M ${start.x} ${start.y} L ${end.x} ${end.y}`}
|
d={`M ${start.x} ${start.y} L ${end.x} ${end.y}`}
|
||||||
fill='none'
|
fill='none'
|
||||||
stroke={theme.palette.background.paper}
|
stroke={theme.palette.background.paper}
|
||||||
@ -65,15 +64,13 @@ const GaugeLines = () => {
|
|||||||
strokeLinecap='round'
|
strokeLinecap='round'
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
key={angle}
|
|
||||||
d={`M ${start.x} ${start.y} L ${end.x} ${end.y}`}
|
d={`M ${start.x} ${start.y} L ${end.x} ${end.y}`}
|
||||||
fill='none'
|
fill='none'
|
||||||
stroke={theme.palette.charts.gauge.sectionLine}
|
stroke={theme.palette.charts.gauge.sectionLine}
|
||||||
strokeWidth={lineWidth - lineBorder}
|
strokeWidth={lineWidth - lineBorder}
|
||||||
strokeLinecap='round'
|
strokeLinecap='round'
|
||||||
/>
|
/>
|
||||||
)
|
</Fragment>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
@ -39,7 +39,7 @@ export const HealthStats: VFC<IHealthStatsProps> = ({
|
|||||||
cy='129'
|
cy='129'
|
||||||
r='121'
|
r='121'
|
||||||
stroke={theme.palette.charts.health.orbit}
|
stroke={theme.palette.charts.health.orbit}
|
||||||
stroke-width='3'
|
strokeWidth='3'
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
x={134}
|
x={134}
|
||||||
@ -149,9 +149,9 @@ export const HealthStats: VFC<IHealthStatsProps> = ({
|
|||||||
width='238'
|
width='238'
|
||||||
height='238'
|
height='238'
|
||||||
filterUnits='userSpaceOnUse'
|
filterUnits='userSpaceOnUse'
|
||||||
color-interpolation-filters='sRGB'
|
colorInterpolationFilters='sRGB'
|
||||||
>
|
>
|
||||||
<feFlood flood-opacity='0' result='BackgroundImageFix' />
|
<feFlood floodOpacity='0' result='BackgroundImageFix' />
|
||||||
<feColorMatrix
|
<feColorMatrix
|
||||||
in='SourceAlpha'
|
in='SourceAlpha'
|
||||||
type='matrix'
|
type='matrix'
|
||||||
@ -190,9 +190,9 @@ export const HealthStats: VFC<IHealthStatsProps> = ({
|
|||||||
width='124'
|
width='124'
|
||||||
height='124'
|
height='124'
|
||||||
filterUnits='userSpaceOnUse'
|
filterUnits='userSpaceOnUse'
|
||||||
color-interpolation-filters='sRGB'
|
colorInterpolationFilters='sRGB'
|
||||||
>
|
>
|
||||||
<feFlood flood-opacity='0' result='BackgroundImageFix' />
|
<feFlood floodOpacity='0' result='BackgroundImageFix' />
|
||||||
<feColorMatrix
|
<feColorMatrix
|
||||||
in='SourceAlpha'
|
in='SourceAlpha'
|
||||||
type='matrix'
|
type='matrix'
|
||||||
@ -225,9 +225,9 @@ export const HealthStats: VFC<IHealthStatsProps> = ({
|
|||||||
width='106'
|
width='106'
|
||||||
height='106'
|
height='106'
|
||||||
filterUnits='userSpaceOnUse'
|
filterUnits='userSpaceOnUse'
|
||||||
color-interpolation-filters='sRGB'
|
colorInterpolationFilters='sRGB'
|
||||||
>
|
>
|
||||||
<feFlood flood-opacity='0' result='BackgroundImageFix' />
|
<feFlood floodOpacity='0' result='BackgroundImageFix' />
|
||||||
<feColorMatrix
|
<feColorMatrix
|
||||||
in='SourceAlpha'
|
in='SourceAlpha'
|
||||||
type='matrix'
|
type='matrix'
|
||||||
@ -260,9 +260,9 @@ export const HealthStats: VFC<IHealthStatsProps> = ({
|
|||||||
width='106'
|
width='106'
|
||||||
height='106'
|
height='106'
|
||||||
filterUnits='userSpaceOnUse'
|
filterUnits='userSpaceOnUse'
|
||||||
color-interpolation-filters='sRGB'
|
colorInterpolationFilters='sRGB'
|
||||||
>
|
>
|
||||||
<feFlood flood-opacity='0' result='BackgroundImageFix' />
|
<feFlood floodOpacity='0' result='BackgroundImageFix' />
|
||||||
<feColorMatrix
|
<feColorMatrix
|
||||||
in='SourceAlpha'
|
in='SourceAlpha'
|
||||||
type='matrix'
|
type='matrix'
|
||||||
@ -297,11 +297,11 @@ export const HealthStats: VFC<IHealthStatsProps> = ({
|
|||||||
gradientUnits='userSpaceOnUse'
|
gradientUnits='userSpaceOnUse'
|
||||||
>
|
>
|
||||||
<stop
|
<stop
|
||||||
stop-color={theme.palette.charts.health.gradientStale}
|
stopColor={theme.palette.charts.health.gradientStale}
|
||||||
/>
|
/>
|
||||||
<stop
|
<stop
|
||||||
offset='1'
|
offset='1'
|
||||||
stop-color={
|
stopColor={
|
||||||
theme.palette.charts.health.gradientPotenciallyStale
|
theme.palette.charts.health.gradientPotenciallyStale
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -1,17 +1,24 @@
|
|||||||
import { ComponentProps, Dispatch, SetStateAction, VFC } from 'react';
|
import { ComponentProps, Dispatch, SetStateAction, VFC } from 'react';
|
||||||
import { Autocomplete, Box, styled, TextField } from '@mui/material';
|
import {
|
||||||
|
Autocomplete,
|
||||||
|
Box,
|
||||||
|
styled,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
import { renderOption } from '../../playground/Playground/PlaygroundForm/renderOption';
|
import { renderOption } from '../../playground/Playground/PlaygroundForm/renderOption';
|
||||||
import useProjects from '../../../hooks/api/getters/useProjects/useProjects';
|
import useProjects from '../../../hooks/api/getters/useProjects/useProjects';
|
||||||
|
|
||||||
const StyledBox = styled(Box)(({ theme }) => ({
|
const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
width: '25%',
|
|
||||||
marginLeft: '75%',
|
|
||||||
marginBottom: theme.spacing(4),
|
marginBottom: theme.spacing(4),
|
||||||
marginTop: theme.spacing(4),
|
marginTop: theme.spacing(4),
|
||||||
[theme.breakpoints.down('lg')]: {
|
[theme.breakpoints.down('lg')]: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
marginLeft: 0,
|
marginLeft: 0,
|
||||||
},
|
},
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface IOption {
|
interface IOption {
|
||||||
@ -74,13 +81,16 @@ export const ProjectSelect: VFC<IProjectSelectProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
|
<Typography variant='h2' component='span'>
|
||||||
|
Insights per project
|
||||||
|
</Typography>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
disablePortal
|
disablePortal
|
||||||
id='projects'
|
id='projects'
|
||||||
limitTags={3}
|
limitTags={3}
|
||||||
multiple={!isAllProjects}
|
multiple={!isAllProjects}
|
||||||
options={projectsOptions}
|
options={projectsOptions}
|
||||||
sx={{ flex: 1 }}
|
sx={{ flex: 1, maxWidth: 360 }}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField {...params} label='Projects' />
|
<TextField {...params} label='Projects' />
|
||||||
)}
|
)}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
|
import React, { type FC } from 'react';
|
||||||
import { ChevronRight } from '@mui/icons-material';
|
import { ChevronRight } from '@mui/icons-material';
|
||||||
import { Box, Typography, styled } from '@mui/material';
|
import { Box, Typography, styled } from '@mui/material';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const StyledUserContainer = styled(Box)(({ theme }) => ({
|
const StyledUserContainer = styled(Box)(({ theme }) => ({
|
||||||
@ -71,10 +71,15 @@ const StyledLink = styled(Link)({
|
|||||||
|
|
||||||
interface IUserStatsProps {
|
interface IUserStatsProps {
|
||||||
count: number;
|
count: number;
|
||||||
|
active?: number;
|
||||||
|
inactive?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserStats: React.FC<IUserStatsProps> = ({ count }) => {
|
export const UserStats: FC<IUserStatsProps> = ({ count, active, inactive }) => {
|
||||||
const showInactiveUsers = useUiFlag('showInactiveUsers');
|
const showInactiveUsers = useUiFlag('showInactiveUsers');
|
||||||
|
const showDistribution =
|
||||||
|
showInactiveUsers && active !== undefined && inactive !== undefined;
|
||||||
|
const activeUsersPercentage = ((active || 0) / count) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -86,23 +91,25 @@ export const UserStats: React.FC<IUserStatsProps> = ({ count }) => {
|
|||||||
</StyledUserContainer>
|
</StyledUserContainer>
|
||||||
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={showInactiveUsers}
|
condition={showDistribution}
|
||||||
show={
|
show={
|
||||||
<>
|
<>
|
||||||
<StyledUserDistributionContainer>
|
<StyledUserDistributionContainer>
|
||||||
<UserDistribution />
|
<UserDistribution
|
||||||
|
activeUsersPercentage={activeUsersPercentage}
|
||||||
|
/>
|
||||||
</StyledUserDistributionContainer>
|
</StyledUserDistributionContainer>
|
||||||
|
|
||||||
<StyledDistInfoContainer>
|
<StyledDistInfoContainer>
|
||||||
<UserDistributionInfo
|
<UserDistributionInfo
|
||||||
type='active'
|
type='active'
|
||||||
percentage='70'
|
percentage={`${activeUsersPercentage}`}
|
||||||
count='9999'
|
count={`${active}`}
|
||||||
/>
|
/>
|
||||||
<UserDistributionInfo
|
<UserDistributionInfo
|
||||||
type='inactive'
|
type='inactive'
|
||||||
percentage='30'
|
percentage={`${100 - activeUsersPercentage}`}
|
||||||
count='9999'
|
count={`${inactive}`}
|
||||||
/>
|
/>
|
||||||
</StyledDistInfoContainer>
|
</StyledDistInfoContainer>
|
||||||
</>
|
</>
|
||||||
@ -135,9 +142,9 @@ const StyledUserDistributionLine = styled(Box)<StyledLinearProgressProps>(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const UserDistribution = () => {
|
const UserDistribution = ({ activeUsersPercentage = 100 }) => {
|
||||||
const getLineWidth = () => {
|
const getLineWidth = () => {
|
||||||
return [80, 20];
|
return [activeUsersPercentage, 100 - activeUsersPercentage];
|
||||||
};
|
};
|
||||||
|
|
||||||
const [activeWidth, inactiveWidth] = getLineWidth();
|
const [activeWidth, inactiveWidth] = getLineWidth();
|
||||||
@ -175,7 +182,6 @@ const StyledUserDistIndicator = styled(Box)<StyledLinearProgressProps>(
|
|||||||
: theme.palette.warning.border,
|
: theme.palette.warning.border,
|
||||||
borderRadius: `2px`,
|
borderRadius: `2px`,
|
||||||
marginRight: theme.spacing(1),
|
marginRight: theme.spacing(1),
|
||||||
marginTop: theme.spacing(0.8),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -208,12 +214,19 @@ const UserDistributionInfo: React.FC<IUserDistributionInfoProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<StyledUserDistContainer>
|
<StyledUserDistContainer>
|
||||||
<StyledUserDistIndicator type={type} />
|
|
||||||
<StyledDistInfoInnerContainer>
|
<StyledDistInfoInnerContainer>
|
||||||
<StyledDistInfoTextContainer>
|
<StyledDistInfoTextContainer>
|
||||||
<Typography variant='body1'>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledUserDistIndicator type={type} />
|
||||||
|
<Typography variant='body1' component='span'>
|
||||||
{type === 'active' ? 'Active' : 'Inactive'} users
|
{type === 'active' ? 'Active' : 'Inactive'} users
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
<Typography variant='body2'>{percentage}%</Typography>
|
<Typography variant='body2'>{percentage}%</Typography>
|
||||||
</StyledDistInfoTextContainer>
|
</StyledDistInfoTextContainer>
|
||||||
<StyledCountTypography variant='h2'>
|
<StyledCountTypography variant='h2'>
|
||||||
|
Loading…
Reference in New Issue
Block a user