1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-07 01:16:28 +02:00

Feat/disabled strategies (#5930)

This PR makes disabled strategies more prominent in the UI:

<img width="1031" alt="Skjermbilde 2024-01-17 kl 11 26 11"
src="https://github.com/Unleash/unleash/assets/16081982/4a07c0aa-8f86-4854-829e-1088abecfb4e">
This commit is contained in:
Fredrik Strand Oseberg 2024-01-17 13:20:39 +01:00 committed by GitHub
parent ee08bd8d42
commit 1deee10317
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 75 additions and 29 deletions

View File

@ -12,6 +12,7 @@ export const ConstraintIcon: VFC<IConstraintIconProps> = ({
disabled, disabled,
}) => ( }) => (
<Box <Box
className='constraint-icon-container'
sx={(theme) => ({ sx={(theme) => ({
backgroundColor: disabled backgroundColor: disabled
? theme.palette.neutral.border ? theme.palette.neutral.border
@ -24,6 +25,7 @@ export const ConstraintIcon: VFC<IConstraintIconProps> = ({
})} })}
> >
<TrackChanges <TrackChanges
className='constraint-icon'
sx={(theme) => ({ sx={(theme) => ({
fill: theme.palette.common.white, fill: theme.palette.common.white,
display: 'block', display: 'block',

View File

@ -15,7 +15,7 @@ const StyledContainer = styled('div')(({ theme }) => ({
lineHeight: 1.25, lineHeight: 1.25,
})); }));
const StyledName = styled('div', { const StyledName = styled('p', {
shouldForwardProp: (prop) => prop !== 'disabled', shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled: boolean }>(({ theme, disabled }) => ({ })<{ disabled: boolean }>(({ theme, disabled }) => ({
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
@ -23,7 +23,7 @@ const StyledName = styled('div', {
color: disabled ? theme.palette.text.secondary : theme.palette.text.primary, color: disabled ? theme.palette.text.secondary : theme.palette.text.primary,
})); }));
const StyledText = styled('div', { const StyledText = styled('p', {
shouldForwardProp: (prop) => prop !== 'disabled', shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled: boolean }>(({ theme, disabled }) => ({ })<{ disabled: boolean }>(({ theme, disabled }) => ({
fontSize: theme.fontSizes.smallerBody, fontSize: theme.fontSizes.smallerBody,

View File

@ -15,7 +15,7 @@ const StyledContainer = styled('div')(({ theme }) => ({
lineHeight: 1.25, lineHeight: 1.25,
})); }));
const StyledName = styled('div', { const StyledName = styled('p', {
shouldForwardProp: (prop) => prop !== 'disabled', shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled: boolean }>(({ theme, disabled }) => ({ })<{ disabled: boolean }>(({ theme, disabled }) => ({
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
@ -23,7 +23,7 @@ const StyledName = styled('div', {
color: disabled ? theme.palette.text.secondary : theme.palette.text.primary, color: disabled ? theme.palette.text.secondary : theme.palette.text.primary,
})); }));
const StyledText = styled('div', { const StyledText = styled('p', {
shouldForwardProp: (prop) => prop !== 'disabled', shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled: boolean }>(({ theme, disabled }) => ({ })<{ disabled: boolean }>(({ theme, disabled }) => ({
fontSize: theme.fontSizes.smallerBody, fontSize: theme.fontSizes.smallerBody,

View File

@ -4,11 +4,13 @@ import { CSSProperties } from 'react';
interface IPercentageCircleProps { interface IPercentageCircleProps {
percentage: number; percentage: number;
size?: `${number}rem`; size?: `${number}rem`;
disabled?: boolean | null;
} }
const PercentageCircle = ({ const PercentageCircle = ({
percentage, percentage,
size = '4rem', size = '4rem',
disabled = false,
}: IPercentageCircleProps) => { }: IPercentageCircleProps) => {
const theme = useTheme(); const theme = useTheme();
@ -27,6 +29,10 @@ const PercentageCircle = ({
const r = 100 / (2 * Math.PI); const r = 100 / (2 * Math.PI);
const d = 2 * r; const d = 2 * r;
const color = disabled
? theme.palette.neutral.border
: theme.palette.primary.light;
return ( return (
<svg viewBox={`0 0 ${d} ${d}`} style={style} aria-hidden> <svg viewBox={`0 0 ${d} ${d}`} style={style} aria-hidden>
<title>A circle progress bar with {percentage}% completion.</title> <title>A circle progress bar with {percentage}% completion.</title>
@ -35,7 +41,7 @@ const PercentageCircle = ({
cx={r} cx={r}
cy={r} cy={r}
fill='none' fill='none'
stroke={theme.palette.primary.light} stroke={color}
strokeWidth={d} strokeWidth={d}
strokeDasharray={`${percentage} 100`} strokeDasharray={`${percentage} 100`}
/> />

View File

@ -16,15 +16,16 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
interface ISegmentItemProps { interface ISegmentItemProps {
segment: Partial<ISegment>; segment: Partial<ISegment>;
isExpanded?: boolean; isExpanded?: boolean;
disabled?: boolean; disabled?: boolean | null;
constraintList?: JSX.Element; constraintList?: JSX.Element;
headerContent?: JSX.Element; headerContent?: JSX.Element;
} }
const StyledAccordion = styled(Accordion)(({ theme }) => ({ const StyledAccordion = styled(Accordion, {
shouldForwardProp: (prop) => prop !== 'isDisabled',
})<{ isDisabled: boolean | null }>(({ theme, isDisabled }) => ({
border: `1px solid ${theme.palette.divider}`, border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadiusMedium, borderRadius: theme.shape.borderRadiusMedium,
backgroundColor: theme.palette.background.paper,
boxShadow: 'none', boxShadow: 'none',
margin: 0, margin: 0,
transition: 'all 0.1s ease', transition: 'all 0.1s ease',
@ -32,6 +33,9 @@ const StyledAccordion = styled(Accordion)(({ theme }) => ({
opacity: '0 !important', opacity: '0 !important',
}, },
'&.Mui-expanded': { backgroundColor: theme.palette.neutral.light }, '&.Mui-expanded': { backgroundColor: theme.palette.neutral.light },
backgroundColor: isDisabled
? theme.palette.envAccordion.disabled
: theme.palette.background.paper,
})); }));
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({ const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
@ -52,7 +56,7 @@ const StyledLink = styled(Link)(({ theme }) => ({
})); }));
const StyledText = styled('span', { const StyledText = styled('span', {
shouldForwardProp: (prop) => prop !== 'disabled', shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled: boolean }>(({ theme, disabled }) => ({ })<{ disabled: boolean | null }>(({ theme, disabled }) => ({
color: disabled ? theme.palette.text.secondary : 'inherit', color: disabled ? theme.palette.text.secondary : 'inherit',
})); }));
@ -66,7 +70,7 @@ export const SegmentItem: VFC<ISegmentItemProps> = ({
const [isOpen, setIsOpen] = useState(isExpanded || false); const [isOpen, setIsOpen] = useState(isExpanded || false);
return ( return (
<StyledAccordion expanded={isOpen}> <StyledAccordion isDisabled={disabled}>
<StyledAccordionSummary id={`segment-accordion-${segment.id}`}> <StyledAccordionSummary id={`segment-accordion-${segment.id}`}>
<DonutLarge <DonutLarge
sx={(theme) => ({ sx={(theme) => ({

View File

@ -6,7 +6,6 @@ import { StrategySeparator } from 'component/common/StrategySeparator/StrategySe
import { ConstraintItem } from './ConstraintItem/ConstraintItem'; import { ConstraintItem } from './ConstraintItem/ConstraintItem';
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { FeatureOverviewSegment } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment'; import { FeatureOverviewSegment } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment';
import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList'; import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
import { import {
@ -23,6 +22,25 @@ interface IStrategyExecutionProps {
strategy: IFeatureStrategyPayload | CreateFeatureStrategySchema; strategy: IFeatureStrategyPayload | CreateFeatureStrategySchema;
} }
const StyledContainer = styled(Box, {
shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled?: boolean | null }>(({ theme, disabled }) => ({
'& p, & span, & h1, & h2, & h3, & h4, & h5, & h6': {
color: disabled ? theme.palette.neutral.main : 'inherit',
},
'.constraint-icon-container': {
backgroundColor: disabled
? theme.palette.neutral.border
: theme.palette.primary.light,
borderRadius: '50%',
},
'.constraint-icon': {
fill: disabled
? theme.palette.neutral.light
: theme.palette.common.white,
},
}));
const NoItems: VFC = () => ( const NoItems: VFC = () => (
<Box sx={{ px: 3, color: 'text.disabled' }}> <Box sx={{ px: 3, color: 'text.disabled' }}>
This strategy does not have constraints or parameters. This strategy does not have constraints or parameters.
@ -44,7 +62,6 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
}) => { }) => {
const { parameters, constraints = [] } = strategy; const { parameters, constraints = [] } = strategy;
const { strategies } = useStrategies(); const { strategies } = useStrategies();
const { uiConfig } = useUiConfig();
const { segments } = useSegments(); const { segments } = useSegments();
const strategySegments = segments?.filter((segment) => { const strategySegments = segments?.filter((segment) => {
return strategy.segments?.includes(segment.id); return strategy.segments?.includes(segment.id);
@ -63,6 +80,8 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
case 'Rollout': { case 'Rollout': {
const percentage = parseParameterNumber(parameters[key]); const percentage = parseParameterNumber(parameters[key]);
const badgeType = strategy.disabled ? 'neutral' : 'success';
return ( return (
<StyledValueContainer <StyledValueContainer
sx={{ display: 'flex', alignItems: 'center' }} sx={{ display: 'flex', alignItems: 'center' }}
@ -71,15 +90,18 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
<PercentageCircle <PercentageCircle
percentage={percentage} percentage={percentage}
size='2rem' size='2rem'
disabled={strategy.disabled}
/> />
</Box> </Box>
<div> <div>
<Badge color='success'>{percentage}%</Badge> of <Badge color={badgeType}>{percentage}%</Badge>{' '}
your base{' '} <span>of your base</span>{' '}
{constraints.length > 0 <span>
? 'who match constraints' {constraints.length > 0
: ''}{' '} ? 'who match constraints'
is included. : ''}{' '}
is included.
</span>
</div> </div>
</StyledValueContainer> </StyledValueContainer>
); );
@ -109,7 +131,7 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
return null; return null;
} }
}); });
}, [parameters, definition, constraints]); }, [parameters, definition, constraints, strategy.disabled]);
const customStrategyList = useMemo(() => { const customStrategyList = useMemo(() => {
if (!parameters || !definition?.editable) return null; if (!parameters || !definition?.editable) return null;
@ -252,7 +274,10 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
const listItems = [ const listItems = [
strategySegments && strategySegments.length > 0 && ( strategySegments && strategySegments.length > 0 && (
<FeatureOverviewSegment segments={strategySegments} /> <FeatureOverviewSegment
segments={strategySegments}
disabled={strategy.disabled}
/>
), ),
constraints.length > 0 && ( constraints.length > 0 && (
<ConstraintAccordionList <ConstraintAccordionList
@ -276,7 +301,7 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
<ConditionallyRender <ConditionallyRender
condition={listItems.length > 0} condition={listItems.length > 0}
show={ show={
<> <StyledContainer disabled={Boolean(strategy.disabled)}>
{listItems.map((item, index) => ( {listItems.map((item, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation> // biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<Fragment key={index}> <Fragment key={index}>
@ -287,7 +312,7 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
{item} {item}
</Fragment> </Fragment>
))} ))}
</> </StyledContainer>
} }
elseShow={<NoItems />} elseShow={<NoItems />}
/> />

View File

@ -13,7 +13,7 @@ import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMen
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer'; import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
import MenuStrategyRemove from './MenuStrategyRemove/MenuStrategyRemove'; import MenuStrategyRemove from './MenuStrategyRemove/MenuStrategyRemove';
import SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider'; import SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider';
import { Box } from '@mui/material';
interface IStrategyItemProps { interface IStrategyItemProps {
environmentId: string; environmentId: string;
strategy: IFeatureStrategy; strategy: IFeatureStrategy;
@ -87,9 +87,16 @@ export const StrategyItem: FC<IStrategyItemProps> = ({
} }
> >
<StrategyExecution strategy={strategy} /> <StrategyExecution strategy={strategy} />
{strategy.variants ? (
<SplitPreviewSlider variants={strategy.variants} /> {strategy.variants &&
) : null} strategy.variants.length > 0 &&
(strategy.disabled ? (
<Box sx={{ opacity: '0.5' }}>
<SplitPreviewSlider variants={strategy.variants} />
</Box>
) : (
<SplitPreviewSlider variants={strategy.variants} />
))}
</StrategyItemContainer> </StrategyItemContainer>
); );
}; };

View File

@ -6,10 +6,12 @@ import { ISegment } from 'interfaces/segment';
interface IFeatureOverviewSegmentProps { interface IFeatureOverviewSegmentProps {
segments?: ISegment[]; segments?: ISegment[];
disabled?: boolean | null;
} }
export const FeatureOverviewSegment = ({ export const FeatureOverviewSegment = ({
segments, segments,
disabled = false,
}: IFeatureOverviewSegmentProps) => { }: IFeatureOverviewSegmentProps) => {
if (!segments || segments.length === 0) { if (!segments || segments.length === 0) {
return null; return null;
@ -23,7 +25,7 @@ export const FeatureOverviewSegment = ({
condition={index > 0} condition={index > 0}
show={<StrategySeparator text='AND' />} show={<StrategySeparator text='AND' />}
/> />
<SegmentItem segment={segment} /> <SegmentItem segment={segment} disabled={disabled} />
</Fragment> </Fragment>
))} ))}
</> </>

View File

@ -35,7 +35,7 @@ exports[`renders an empty list correctly 1`] = `
className="css-non55o" className="css-non55o"
> >
<div <div
className="css-16ldy6v" className="css-1om4ep4"
> >
<svg <svg
aria-hidden={true} aria-hidden={true}

View File

@ -137,7 +137,7 @@ export const theme = {
main: colors.grey[700], main: colors.grey[700],
light: colors.grey[100], light: colors.grey[100],
dark: colors.grey[800], dark: colors.grey[800],
border: colors.grey[400], border: colors.grey[500],
contrastText: colors.grey[800], // Color used for text inside badge contrastText: colors.grey[800], // Color used for text inside badge
}, },