diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx index 9d209f2b4a97..d66cfd2eb3ee 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView.tsx @@ -23,6 +23,7 @@ interface IConstraintAccordionViewProps { onEdit?: () => void; sx?: SxProps; compact?: boolean; + disabled?: boolean; renderAfter?: JSX.Element; } @@ -68,6 +69,7 @@ export const ConstraintAccordionView = ({ onDelete, sx = undefined, compact = false, + disabled = false, renderAfter, }: IConstraintAccordionViewProps) => { const [expandable, setExpandable] = useState(true); @@ -102,6 +104,7 @@ export const ConstraintAccordionView = ({ onDelete={onDelete} singleValue={singleValue} allowExpand={setExpandable} + disabled={disabled} expanded={expanded} compact={compact} /> diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeader.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeader.tsx index 96576d805288..65145ce7c06f 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeader.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeader.tsx @@ -13,6 +13,7 @@ interface IConstraintAccordionViewHeaderProps { expanded: boolean; allowExpand: (shouldExpand: boolean) => void; compact?: boolean; + disabled?: boolean; } const StyledContainer = styled('div')(({ theme }) => ({ @@ -34,6 +35,7 @@ export const ConstraintAccordionViewHeader = ({ allowExpand, expanded, compact, + disabled, }: IConstraintAccordionViewHeaderProps) => { const { context } = useUnleashContext(); const { contextName } = constraint; @@ -44,12 +46,13 @@ export const ConstraintAccordionViewHeader = ({ return ( - + void; + disabled?: boolean; maxLength?: number; } @@ -58,23 +59,34 @@ export const ConstraintAccordionViewHeaderInfo = ({ singleValue, allowExpand, expanded, + disabled = false, maxLength = 112, //The max number of characters in the values text for NOT allowing expansion }: ConstraintAccordionViewHeaderMetaInfoProps) => { return ( - + ({ + color: disabled + ? theme.palette.text.secondary + : 'inherit', + })} + > {constraint.contextName} - + } elseShow={ @@ -83,6 +95,7 @@ export const ConstraintAccordionViewHeaderInfo = ({ expanded={expanded} allowExpand={allowExpand} maxLength={maxLength} + disabled={disabled} /> } /> diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderMultipleValues.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderMultipleValues.tsx index 6f70f0dfa522..f819e8052cad 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderMultipleValues.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeaderMultipleValues.tsx @@ -22,6 +22,7 @@ interface ConstraintSingleValueProps { expanded: boolean; maxLength: number; allowExpand: (shouldExpand: boolean) => void; + disabled?: boolean; } const StyledHeaderValuesContainerWrapper = styled('div')(({ theme }) => ({ @@ -55,6 +56,7 @@ export const ConstraintAccordionViewHeaderMultipleValues = ({ expanded, allowExpand, maxLength, + disabled = false, }: ConstraintSingleValueProps) => { const [expandable, setExpandable] = useState(false); @@ -72,7 +74,15 @@ export const ConstraintAccordionViewHeaderMultipleValues = ({ return ( - {text} + ({ + color: disabled + ? theme.palette.text.secondary + : 'inherit', + })} + > + {text} + ({ interface ConstraintSingleValueProps { constraint: IConstraint; allowExpand: (shouldExpand: boolean) => void; + disabled?: boolean; } const StyledHeaderValuesContainerWrapper = styled('div')(({ theme }) => ({ @@ -26,6 +27,7 @@ const StyledHeaderValuesContainerWrapper = styled('div')(({ theme }) => ({ export const ConstraintAccordionViewHeaderSingleValue = ({ constraint, allowExpand, + disabled = false, }: ConstraintSingleValueProps) => { const { locationSettings } = useLocationSettings(); @@ -36,6 +38,9 @@ export const ConstraintAccordionViewHeaderSingleValue = ({ return ( ({ + color: disabled ? theme.palette.text.secondary : 'inherit', + })} label={formatConstraintValue(constraint, locationSettings)} /> diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintViewHeaderOperator.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintViewHeaderOperator.tsx index 2a5d3e6c9da0..5ba0ab66d7c2 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintViewHeaderOperator.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintViewHeaderOperator.tsx @@ -10,6 +10,7 @@ import { oneOf } from 'utils/oneOf'; interface ConstraintViewHeaderOperatorProps { constraint: IConstraint; + disabled?: boolean; } const StyledHeaderValuesContainerWrapper = styled('div')(({ theme }) => ({ @@ -28,6 +29,7 @@ const StyledHeaderConstraintContainer = styled('div')(({ theme }) => ({ export const ConstraintViewHeaderOperator = ({ constraint, + disabled = false, }: ConstraintViewHeaderOperatorProps) => { return ( @@ -47,6 +49,7 @@ export const ConstraintViewHeaderOperator = ({ = ({ compact }) => ( +export const ConstraintIcon: VFC = ({ + compact, + disabled, +}) => ( ({ + backgroundColor: disabled + ? theme.palette.neutral.border + : 'primary.light', p: compact ? '1px' : '2px', borderRadius: '50%', width: compact ? '18px' : '24px', height: compact ? '18px' : '24px', marginRight: '13px', - }} + })} > ({ diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator.tsx index b7bd3fff05d4..0e05726d3300 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator.tsx @@ -6,6 +6,7 @@ import { styled } from '@mui/material'; interface IConstraintOperatorProps { constraint: IConstraint; hasPrefix?: boolean; + disabled?: boolean; } const StyledContainer = styled('div')(({ theme }) => ({ @@ -15,19 +16,25 @@ const StyledContainer = styled('div')(({ theme }) => ({ lineHeight: 1.25, })); -const StyledName = styled('div')(({ theme }) => ({ +const StyledName = styled('div', { + shouldForwardProp: (prop) => prop !== 'disabled', +})<{ disabled: boolean }>(({ theme, disabled }) => ({ fontSize: theme.fontSizes.smallBody, lineHeight: 17 / 14, + color: disabled ? theme.palette.text.secondary : theme.palette.text.primary, })); -const StyledText = styled('div')(({ theme }) => ({ +const StyledText = styled('div', { + shouldForwardProp: (prop) => prop !== 'disabled', +})<{ disabled: boolean }>(({ theme, disabled }) => ({ fontSize: theme.fontSizes.smallerBody, - color: theme.palette.neutral.main, + color: disabled ? theme.palette.text.secondary : theme.palette.neutral.main, })); export const ConstraintOperator = ({ constraint, hasPrefix, + disabled = false, }: IConstraintOperatorProps) => { const operatorName = constraint.operator; const operatorText = formatOperatorDescription(constraint.operator); @@ -40,8 +47,8 @@ export const ConstraintOperator = ({ paddingLeft: hasPrefix ? 0 : undefined, }} > - {operatorName} - {operatorText} + {operatorName} + {operatorText} ); }; diff --git a/frontend/src/component/common/PercentageCircle/DisabledPercentageCircle.tsx b/frontend/src/component/common/PercentageCircle/DisabledPercentageCircle.tsx new file mode 100644 index 000000000000..9f8105ac15a5 --- /dev/null +++ b/frontend/src/component/common/PercentageCircle/DisabledPercentageCircle.tsx @@ -0,0 +1,46 @@ +import { useTheme } from '@mui/material'; +import { CSSProperties } from 'react'; + +interface IPercentageCircleProps { + percentage: number; + size?: `${number}rem`; +} + +const PercentageCircle = ({ + percentage, + size = '4rem', +}: IPercentageCircleProps) => { + const theme = useTheme(); + + const style: CSSProperties = { + display: 'block', + borderRadius: '100%', + transform: 'rotate(-90deg)', + height: size, + width: size, + background: theme.palette.background.elevation2, + }; + + // The percentage circle used to be drawn by CSS with a conic-gradient, + // but the result was either jagged or blurry. SVG seems to look better. + // See https://stackoverflow.com/a/70659532. + const radius = 100 / (2 * Math.PI); + const diameter = 2 * radius; + + return ( + + A circle progress bar with {percentage}% completion. + + + ); +}; + +export default PercentageCircle; diff --git a/frontend/src/component/common/SegmentItem/SegmentItem.tsx b/frontend/src/component/common/SegmentItem/SegmentItem.tsx index b085e80af644..061ee6dacfe7 100644 --- a/frontend/src/component/common/SegmentItem/SegmentItem.tsx +++ b/frontend/src/component/common/SegmentItem/SegmentItem.tsx @@ -16,6 +16,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit interface ISegmentItemProps { segment: Partial; isExpanded?: boolean; + disabled?: boolean; constraintList?: JSX.Element; headerContent?: JSX.Element; } @@ -49,20 +50,33 @@ const StyledLink = styled(Link)(({ theme }) => ({ textDecoration: 'underline', }, })); +const StyledText = styled('span', { + shouldForwardProp: (prop) => prop !== 'disabled', +})<{ disabled: boolean }>(({ theme, disabled }) => ({ + color: disabled ? theme.palette.text.secondary : 'inherit', +})); export const SegmentItem: VFC = ({ segment, isExpanded, headerContent, constraintList, + disabled = false, }) => { const [isOpen, setIsOpen] = useState(isExpanded || false); return ( - - Segment: + ({ + mr: 1, + color: disabled + ? theme.palette.neutral.border + : theme.palette.secondary.main, + })} + /> + Segment: {segment.name} diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintExecutionWithoutResults.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintExecutionWithoutResults.tsx index bcfbc342c070..862270d1f07d 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintExecutionWithoutResults.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintExecutionWithoutResults.tsx @@ -9,8 +9,6 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { styled } from '@mui/material'; import { ConstraintAccordionView } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView'; -import { ConstraintError } from './ConstraintError/ConstraintError'; -import { ConstraintOk } from './ConstraintOk/ConstraintOk'; interface IConstraintExecutionWithoutResultsProps { constraints?: PlaygroundConstraintSchema[]; @@ -35,7 +33,11 @@ export const ConstraintExecutionWithoutResults: VFC< condition={index > 0} show={} /> - + ))} diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/DisabledStrategyExecution.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/DisabledStrategyExecution.tsx index f195bd8f0f9a..391b9649207d 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/DisabledStrategyExecution.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/DisabledStrategyExecution.tsx @@ -52,6 +52,7 @@ export const DisabledStrategyExecution: VFC = parameters={parameters} constraints={constraints} input={input} + disabled /> ), hasCustomStrategyParameters && ( @@ -61,9 +62,14 @@ export const DisabledStrategyExecution: VFC = /> ), name === 'default' && ( - - The standard strategy is ON{' '} - for all users. + ({ + width: '100%', + color: theme.palette.text.secondary, + })} + > + The standard strategy is{' '} + ON for all users. ), ].filter(Boolean); @@ -74,7 +80,12 @@ export const DisabledStrategyExecution: VFC = // biome-ignore lint/suspicious/noArrayIndexKey: 0} + condition={ + index > 0 && + (strategyResult.name === 'flexibleRollout' + ? index < items.length + : index < items.length - 1) + } show={} /> {item} diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/PlaygroundParameterItem/PlaygroundParameterItem.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/PlaygroundParameterItem/PlaygroundParameterItem.tsx index a722cef287c5..5c2bb84a944e 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/PlaygroundParameterItem/PlaygroundParameterItem.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/PlaygroundParameterItem/PlaygroundParameterItem.tsx @@ -8,6 +8,7 @@ interface IConstraintItemProps { text: string; input?: string | number | boolean | 'no value'; showReason?: boolean; + disabled?: boolean; } const StyledDivContainer = styled('div', { @@ -34,12 +35,15 @@ const StyledChip = styled(Chip)(({ theme }) => ({ margin: theme.spacing(0.5), })); -const StyledParagraph = styled('p')(({ theme }) => ({ +const StyledParagraph = styled('p', { + shouldForwardProp: (prop) => prop !== 'disabled', +})<{ disabled: boolean }>(({ theme, disabled }) => ({ display: 'inline', margin: theme.spacing(0.5, 0), maxWidth: '95%', textAlign: 'center', wordBreak: 'break-word', + color: disabled ? theme.palette.text.secondary : 'inherit', })); export const PlaygroundParameterItem = ({ @@ -47,10 +51,11 @@ export const PlaygroundParameterItem = ({ text, input, showReason = false, + disabled = false, }: IConstraintItemProps) => { const theme = useTheme(); - const color = input === 'no value' ? 'error' : 'neutral'; + const color = input === 'no value' && !disabled ? 'error' : 'neutral'; const reason = `value does not match any ${text}`; return ( @@ -64,7 +69,11 @@ export const PlaygroundParameterItem = ({ show={ {reason} @@ -75,7 +84,7 @@ export const PlaygroundParameterItem = ({ show={

No {text}s added yet.

} elseShow={
- + {value.length}{' '} {value.length > 1 ? `${text}s` : text} will get access. @@ -83,6 +92,7 @@ export const PlaygroundParameterItem = ({ {value.map((v: string | number) => ( } + show={ + + } elseShow={
} /> diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/SegmentExecution/SegmentExecutionWithoutResult.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/SegmentExecution/SegmentExecutionWithoutResult.tsx index 33e40ac84c38..aadc0b12c69e 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/SegmentExecution/SegmentExecutionWithoutResult.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/SegmentExecution/SegmentExecutionWithoutResult.tsx @@ -29,6 +29,7 @@ export const SegmentExecutionWithoutResult: VFC< /> } isExpanded + disabled /> = ({ // biome-ignore lint/suspicious/noArrayIndexKey: 0} + condition={ + index > 0 && + (strategyResult.name === 'flexibleRollout' + ? index < items.length + : index < items.length - 1) + } show={} /> {item} diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecutionParameters/StrategyExecutionParameters.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecutionParameters/StrategyExecutionParameters.tsx index cc5606da0812..d96c0a5895eb 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecutionParameters/StrategyExecutionParameters.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecutionParameters/StrategyExecutionParameters.tsx @@ -2,24 +2,34 @@ import { parseParameterNumber, parseParameterStrings, } from 'utils/parseParameter'; -import { Box } from '@mui/material'; +import { Box, styled } from '@mui/material'; import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle'; import { PlaygroundParameterItem } from '../PlaygroundParameterItem/PlaygroundParameterItem'; import { StyledBoxSummary } from '../StrategyExecution.styles'; import { PlaygroundConstraintSchema, PlaygroundRequestSchema } from 'openapi'; import { getMappedParam } from '../helpers'; import { Badge } from 'component/common/Badge/Badge'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import DisabledPercentageCircle from 'component/common/PercentageCircle/DisabledPercentageCircle'; export interface PlaygroundResultStrategyExecutionParametersProps { parameters: { [key: string]: string }; constraints: PlaygroundConstraintSchema[]; input?: PlaygroundRequestSchema; + disabled?: boolean; } +const StyledText = styled('div', { + shouldForwardProp: (prop) => prop !== 'disabled', +})<{ disabled: boolean }>(({ theme, disabled }) => ({ + color: disabled ? theme.palette.text.secondary : theme.palette.neutral.main, +})); + export const PlaygroundResultStrategyExecutionParameters = ({ parameters, constraints, input, + disabled = false, }: PlaygroundResultStrategyExecutionParametersProps) => { return ( <> @@ -35,20 +45,44 @@ export const PlaygroundResultStrategyExecutionParameters = ({ key={key} sx={{ display: 'flex', alignItems: 'center' }} > - - ({ + mr: '1rem', + color: disabled + ? theme.palette.neutral.border + : theme.palette.text.secondary, + })} + > + + } + elseShow={ + + } /> -
- {percentage}%{' '} + + + {percentage}% + {' '} of your base{' '} {constraints.length > 0 ? 'who match constraints' : ''}{' '} is included. -
+ ); } @@ -87,6 +121,7 @@ export const PlaygroundResultStrategyExecutionParameters = ({ text={'host'} input={'no value'} showReason={undefined} + disabled={disabled} /> ); } @@ -97,6 +132,7 @@ export const PlaygroundResultStrategyExecutionParameters = ({ key={key} value={IPs} text={'IP'} + disabled={disabled} input={ input?.context?.[getMappedParam(key)] ? input?.context?.[getMappedParam(key)]