Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1-3083: add remaining lifecycle header + tooltip #8722

Merged
merged 11 commits into from
Nov 13, 2024
28 changes: 23 additions & 5 deletions frontend/src/component/common/HelpIcon/HelpIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { styled, Tooltip, type TooltipProps } from '@mui/material';
import HelpOutline from '@mui/icons-material/HelpOutline';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import {
HtmlTooltip,
type IHtmlTooltipProps,
} from 'component/common/HtmlTooltip/HtmlTooltip';

const StyledContainer = styled('span')<{ size: string | undefined }>(
({ theme, size }) => ({
Expand All @@ -24,24 +27,39 @@ const StyledContainer = styled('span')<{ size: string | undefined }>(
}),
);

interface IHelpIconProps {
type IHelpIconProps = {
tooltip: React.ReactNode;
htmlTooltip?: boolean;
placement?: TooltipProps['placement'];
children?: React.ReactNode;
size?: string;
}
} & (
| {
htmlTooltip: true;
htmlTooltipMaxWidth?: IHtmlTooltipProps['maxWidth'];
}
| { htmlTooltip?: false }
);

export const HelpIcon = ({
tooltip,
htmlTooltip,
placement,
children,
size,
...props
}: IHelpIconProps) => {
if (htmlTooltip) {
const { htmlTooltipMaxWidth } = props as {
htmlTooltipMaxWidth?: IHtmlTooltipProps['maxWidth'];
};

return (
<HtmlTooltip title={tooltip} placement={placement} arrow>
<HtmlTooltip
title={tooltip}
placement={placement}
arrow
maxWidth={htmlTooltipMaxWidth}
>
<StyledContainer size={size} tabIndex={0} aria-label='Help'>
{children ?? <HelpOutline />}
</StyledContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import type { FC } from 'react';
import { PrettifyLargeNumber } from 'component/common/PrettifyLargeNumber/PrettifyLargeNumber';
import type { ProjectStatusSchemaLifecycleSummary } from 'openapi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';

const LifecycleRow = styled('div')(({ theme }) => ({
display: 'flex',
flexFlow: 'column',
gap: theme.spacing(1),
}));

const HeaderRow = styled('div')(({ theme }) => ({
display: 'flex',
flex: 'auto',
'& > *': {
marginBlock: 0,
fontWeight: 'normal',
},
}));

const LifecycleBox = styled('li')(({ theme }) => ({
padding: theme.spacing(2),
borderRadius: theme.shape.borderRadiusExtraLarge,
Expand All @@ -16,13 +34,14 @@ const LifecycleBox = styled('li')(({ theme }) => ({
justifyContent: 'space-between',
}));

const Wrapper = styled('ul')(({ theme }) => ({
const LifecycleList = styled('ul')(({ theme }) => ({
display: 'grid',
listStyle: 'none',
gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))',
gap: theme.spacing(1),
justifyContent: 'center',
padding: 0,
flex: 'auto',
}));

const Counter = styled('span')({
Expand Down Expand Up @@ -83,9 +102,47 @@ const BigNumber: FC<{ value?: number }> = ({ value }) => {
);
};

const TooltipContent = styled('div')(({ theme }) => ({
padding: theme.spacing(0.5),
}));

const TooltipText = styled('p')(({ theme }) => ({
fontSize: theme.typography.body1.fontSize,
'& + p': {
marginTop: theme.spacing(1),
},
}));

const LifecycleTooltip: FC = () => {
return (
<HelpIcon
htmlTooltip
htmlTooltipMaxWidth='550px'
tooltip={
<TooltipContent>
<TooltipText>
Based on usage metrics and interactions with Unleash,
feature flags can go through five distinct lifecycle
stages. These stages mirror the typical software
development process and allow you to identify
bottlenecks at any stage of the lifecycle.
</TooltipText>

<TooltipText>
<a href='https://docs.getunleash.io/reference/feature-toggles#feature-flag-lifecycle'>
Read more in our documentation
</a>
</TooltipText>
</TooltipContent>
}
/>
);
};

export const ProjectLifecycleSummary = () => {
const projectId = useRequiredPathParam('projectId');
const { data, loading } = useProjectStatus(projectId);
const { isEnterprise } = useUiConfig();

const loadingRef = useLoading<HTMLUListElement>(
loading,
Expand All @@ -99,103 +156,118 @@ export const ProjectLifecycleSummary = () => {
}
};
return (
<Wrapper ref={loadingRef}>
<LifecycleBox>
<p>
<Counter>
<BigNumber
value={data?.lifecycleSummary.initial.currentFlags}
/>

<FeatureLifecycleStageIcon
aria-hidden='true'
stage={{ name: 'initial' }}
/>
</Counter>
<span>{flagWord('initial')} in initial</span>
</p>
<AverageDaysStat
averageDays={data?.lifecycleSummary.initial.averageDays}
/>
</LifecycleBox>
<LifecycleBox>
<p>
<Counter>
<BigNumber
value={data?.lifecycleSummary.preLive.currentFlags}
/>

<FeatureLifecycleStageIcon
aria-hidden='true'
stage={{ name: 'pre-live' }}
/>
</Counter>
<span>{flagWord('preLive')} in pre-live</span>
</p>
<AverageDaysStat
averageDays={data?.lifecycleSummary.preLive.averageDays}
/>
</LifecycleBox>
<LifecycleBox>
<p>
<Counter>
<BigNumber
value={data?.lifecycleSummary.live.currentFlags}
/>

<FeatureLifecycleStageIcon
aria-hidden='true'
stage={{ name: 'live' }}
/>
</Counter>
<span>{flagWord('live')} in live</span>
</p>
<AverageDaysStat
averageDays={data?.lifecycleSummary.live.averageDays}
/>
</LifecycleBox>
<LifecycleBox>
<p>
<Counter>
<BigNumber
value={
data?.lifecycleSummary.completed.currentFlags
}
/>

<FeatureLifecycleStageIcon
aria-hidden='true'
stage={{ name: 'completed' }}
/>
</Counter>
<span>{flagWord('completed')} in cleanup</span>
</p>
<AverageDaysStat
averageDays={data?.lifecycleSummary.completed.averageDays}
/>
</LifecycleBox>
<LifecycleBox>
<p>
<Counter>
<BigNumber
value={data?.lifecycleSummary.archived.currentFlags}
/>

<FeatureLifecycleStageIcon
aria-hidden='true'
stage={{ name: 'archived' }}
/>
</Counter>
<span>{flagWord('archived')} in archived</span>
</p>
<Stats>
<dt>This month</dt>
<dd data-loading-project-lifecycle-summary>
{data?.lifecycleSummary.archived.currentFlags ?? 0}{' '}
flags archived
</dd>
</Stats>
</LifecycleBox>
</Wrapper>
<LifecycleRow>
<HeaderRow>
<h4>Flag lifecycle</h4>
<LifecycleTooltip />
</HeaderRow>
<LifecycleList ref={loadingRef}>
<LifecycleBox>
<p>
<Counter>
<BigNumber
value={
data?.lifecycleSummary.initial.currentFlags
}
/>

<FeatureLifecycleStageIcon
aria-hidden='true'
stage={{ name: 'initial' }}
/>
</Counter>
<span>{flagWord('initial')} in initial</span>
</p>
<AverageDaysStat
averageDays={data?.lifecycleSummary.initial.averageDays}
/>
</LifecycleBox>
<LifecycleBox>
<p>
<Counter>
<BigNumber
value={
data?.lifecycleSummary.preLive.currentFlags
}
/>

<FeatureLifecycleStageIcon
aria-hidden='true'
stage={{ name: 'pre-live' }}
/>
</Counter>
<span>{flagWord('preLive')} in pre-live</span>
</p>
<AverageDaysStat
averageDays={data?.lifecycleSummary.preLive.averageDays}
/>
</LifecycleBox>
<LifecycleBox>
<p>
<Counter>
<BigNumber
value={data?.lifecycleSummary.live.currentFlags}
/>

<FeatureLifecycleStageIcon
aria-hidden='true'
stage={{ name: 'live' }}
/>
</Counter>
<span>{flagWord('live')} in live</span>
</p>
<AverageDaysStat
averageDays={data?.lifecycleSummary.live.averageDays}
/>
</LifecycleBox>
<LifecycleBox>
<p>
<Counter>
<BigNumber
value={
data?.lifecycleSummary.completed
.currentFlags
}
/>

<FeatureLifecycleStageIcon
aria-hidden='true'
stage={{ name: 'completed' }}
/>
</Counter>
<span>{flagWord('completed')} in cleanup</span>
</p>
<AverageDaysStat
averageDays={
data?.lifecycleSummary.completed.averageDays
}
/>
</LifecycleBox>
<LifecycleBox>
<p>
<Counter>
<BigNumber
value={
data?.lifecycleSummary.archived.currentFlags
}
/>

<FeatureLifecycleStageIcon
aria-hidden='true'
stage={{ name: 'archived' }}
/>
</Counter>
<span>{flagWord('archived')} in archived</span>
</p>
<Stats>
<dt>This month</dt>
<dd data-loading-project-lifecycle-summary>
{data?.lifecycleSummary.archived.currentFlags ?? 0}{' '}
flags archived
</dd>
</Stats>
</LifecycleBox>
</LifecycleList>
</LifecycleRow>
);
};
Loading