From a6cfcea0299f562f4d6cefd46cad8275ea25c44d Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Fri, 28 Feb 2025 10:49:23 +0100 Subject: [PATCH] refactor: new constraints style (#9363) Refactored styles for strategy evaluation parameters. New look for constraints etc --- .../common/SegmentItem/SegmentItem.tsx | 1 + .../StrategySeparator/StrategySeparator.tsx | 4 +- .../ConstraintItem/ConstraintItem.tsx | 96 ++-- .../ConstraintItem/LegacyConstraintItem.tsx | 60 +++ .../LegacyStrategyExecution.tsx | 372 +++++++++++++++ .../StrategyEvaluationChip.tsx | 18 + .../StrategyEvaluationItem.tsx | 57 +++ .../StrategyEvaluationSeparator.tsx | 18 + .../StrategyExecution/StrategyExecution.tsx | 430 ++++-------------- .../hooks/useCustomStrategyParameters.tsx | 123 +++++ .../hooks/useStrategyParameters.tsx | 92 ++++ .../FeatureOverviewMetaData/TagRow.tsx | 1 + 12 files changed, 871 insertions(+), 401 deletions(-) create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/LegacyConstraintItem.tsx create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/LegacyStrategyExecution.tsx create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyEvaluationChip/StrategyEvaluationChip.tsx create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyEvaluationItem/StrategyEvaluationItem.tsx create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyEvaluationSeparator/StrategyEvaluationSeparator.tsx create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useCustomStrategyParameters.tsx create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useStrategyParameters.tsx diff --git a/frontend/src/component/common/SegmentItem/SegmentItem.tsx b/frontend/src/component/common/SegmentItem/SegmentItem.tsx index 2a4904a232..bae81d53cf 100644 --- a/frontend/src/component/common/SegmentItem/SegmentItem.tsx +++ b/frontend/src/component/common/SegmentItem/SegmentItem.tsx @@ -56,6 +56,7 @@ const StyledLink = styled(Link)(({ theme }) => ({ textDecoration: 'underline', }, })); + const StyledText = styled('span', { shouldForwardProp: (prop) => prop !== 'disabled', })<{ disabled: boolean | null }>(({ theme, disabled }) => ({ diff --git a/frontend/src/component/common/StrategySeparator/StrategySeparator.tsx b/frontend/src/component/common/StrategySeparator/StrategySeparator.tsx index c384668f25..9cbf96d745 100644 --- a/frontend/src/component/common/StrategySeparator/StrategySeparator.tsx +++ b/frontend/src/component/common/StrategySeparator/StrategySeparator.tsx @@ -9,9 +9,7 @@ const Chip = styled('div')(({ theme }) => ({ transform: 'translateY(-50%)', lineHeight: 1, borderRadius: theme.shape.borderRadiusLarge, - fontWeight: 'bold', - backgroundColor: theme.palette.background.alternative, - color: theme.palette.primary.contrastText, + backgroundColor: theme.palette.secondary.border, left: theme.spacing(4), })); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/ConstraintItem.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/ConstraintItem.tsx index 91680bc098..2a038b3315 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/ConstraintItem.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/ConstraintItem.tsx @@ -1,60 +1,52 @@ -import { Chip, styled } from '@mui/material'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import StringTruncator from 'component/common/StringTruncator/StringTruncator'; +import type { FC } from 'react'; +import { StrategyEvaluationItem } from '../StrategyEvaluationItem/StrategyEvaluationItem'; +import type { ConstraintSchema } from 'openapi'; +import { formatOperatorDescription } from 'component/common/ConstraintAccordion/ConstraintOperator/formatOperatorDescription'; +import { StrategyEvaluationChip } from '../StrategyEvaluationChip/StrategyEvaluationChip'; +import { styled, Tooltip } from '@mui/material'; -interface IConstraintItemProps { - value: string[]; - text: string; -} +const Inverted: FC = () => ( + + + +); -const StyledContainer = styled('div')(({ theme }) => ({ - width: '100%', - padding: theme.spacing(2, 3), - borderRadius: theme.shape.borderRadiusMedium, - background: theme.palette.background.default, - border: `1px solid ${theme.palette.divider}`, +const Operator: FC<{ label: ConstraintSchema['operator'] }> = ({ label }) => ( + + + +); + +const CaseInsensitive: FC = () => ( + + Aa} /> + +); + +const StyledOperatorGroup = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(0.5), })); -const StyledParagraph = styled('p')(({ theme }) => ({ - display: 'inline', - margin: theme.spacing(0.5, 0), - maxWidth: '95%', - textAlign: 'center', - wordBreak: 'break-word', -})); +export const ConstraintItem: FC = ({ + caseInsensitive, + contextName, + inverted, + operator, + value, + values, +}) => { + const items = value ? [value, ...(values || [])] : values || []; -const StyledChip = styled(Chip)(({ theme }) => ({ - margin: theme.spacing(0.5), -})); - -export const ConstraintItem = ({ value, text }: IConstraintItemProps) => { return ( - - No {text}s added yet.

} - elseShow={ -
- - {value.length}{' '} - {value.length > 1 ? `${text}s` : text} will get - access. - - {value.map((v: string) => ( - - } - /> - ))} -
- } - /> -
+ + {contextName} + + {inverted ? : null} + + {caseInsensitive ? : null} + + ); }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/LegacyConstraintItem.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/LegacyConstraintItem.tsx new file mode 100644 index 0000000000..91680bc098 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/ConstraintItem/LegacyConstraintItem.tsx @@ -0,0 +1,60 @@ +import { Chip, styled } from '@mui/material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import StringTruncator from 'component/common/StringTruncator/StringTruncator'; + +interface IConstraintItemProps { + value: string[]; + text: string; +} + +const StyledContainer = styled('div')(({ theme }) => ({ + width: '100%', + padding: theme.spacing(2, 3), + borderRadius: theme.shape.borderRadiusMedium, + background: theme.palette.background.default, + border: `1px solid ${theme.palette.divider}`, +})); + +const StyledParagraph = styled('p')(({ theme }) => ({ + display: 'inline', + margin: theme.spacing(0.5, 0), + maxWidth: '95%', + textAlign: 'center', + wordBreak: 'break-word', +})); + +const StyledChip = styled(Chip)(({ theme }) => ({ + margin: theme.spacing(0.5), +})); + +export const ConstraintItem = ({ value, text }: IConstraintItemProps) => { + return ( + + No {text}s added yet.

} + elseShow={ +
+ + {value.length}{' '} + {value.length > 1 ? `${text}s` : text} will get + access. + + {value.map((v: string) => ( + + } + /> + ))} +
+ } + /> +
+ ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/LegacyStrategyExecution.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/LegacyStrategyExecution.tsx new file mode 100644 index 0000000000..a73357e44e --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/LegacyStrategyExecution.tsx @@ -0,0 +1,372 @@ +import { type FC, Fragment, useMemo } from 'react'; +import { Alert, Box, Chip, Link, styled } from '@mui/material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle'; +import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; +import { ConstraintItem } from './ConstraintItem/LegacyConstraintItem'; +import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; +import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; +import { FeatureOverviewSegment } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment'; +import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList'; +import { + parseParameterNumber, + parseParameterString, + parseParameterStrings, +} from 'utils/parseParameter'; +import StringTruncator from 'component/common/StringTruncator/StringTruncator'; +import { Badge } from 'component/common/Badge/Badge'; +import type { CreateFeatureStrategySchema } from 'openapi'; +import type { IFeatureStrategyPayload } from 'interfaces/strategy'; +import { BuiltInStrategies } from 'utils/strategyNames'; + +interface IStrategyExecutionProps { + strategy: IFeatureStrategyPayload | CreateFeatureStrategySchema; + displayGroupId?: boolean; +} + +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 CustomStrategyDeprecationWarning = () => ( + + Custom strategies are deprecated and may be removed in a future major + version. Consider rewriting this strategy as a predefined strategy with{' '} + + constraints. + + +); + +const NoItems: FC = () => ( + + This strategy does not have constraints or parameters. + +); + +const StyledValueContainer = styled(Box)(({ theme }) => ({ + padding: theme.spacing(2, 3), + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadiusMedium, + background: theme.palette.background.default, +})); + +const StyledValueSeparator = styled('span')(({ theme }) => ({ + color: theme.palette.neutral.main, +})); + +export const StrategyExecution: FC = ({ + strategy, + displayGroupId = false, +}) => { + const { parameters, constraints = [] } = strategy; + const stickiness = parameters?.stickiness; + const explainStickiness = + typeof stickiness === 'string' && stickiness !== 'default'; + const { strategies } = useStrategies(); + const { segments } = useSegments(); + const strategySegments = segments?.filter((segment) => { + return strategy.segments?.includes(segment.id); + }); + + const definition = strategies.find((strategyDefinition) => { + return strategyDefinition.name === strategy.name; + }); + + const parametersList = useMemo(() => { + if (!parameters || definition?.editable) return null; + + return Object.keys(parameters).map((key) => { + switch (key) { + case 'rollout': + case 'Rollout': { + const percentage = parseParameterNumber(parameters[key]); + + const badgeType = strategy.disabled ? 'neutral' : 'success'; + + return ( + + + + +
+ {percentage}%{' '} + of your base{' '} + + {explainStickiness ? ( + <> + with {stickiness} + + ) : ( + '' + )}{' '} + + + {constraints.length > 0 + ? 'who match constraints' + : ''}{' '} + is included. + +
+ {displayGroupId && parameters.groupId && ( + ({ + ml: 1, + color: theme.palette.info.contrastText, + })} + > + + GroupId: {parameters.groupId} + + + )} +
+ ); + } + case 'userIds': + case 'UserIds': { + const users = parseParameterStrings(parameters[key]); + return ( + + ); + } + case 'hostNames': + case 'HostNames': { + const hosts = parseParameterStrings(parameters[key]); + return ( + + ); + } + case 'IPs': { + const IPs = parseParameterStrings(parameters[key]); + return ; + } + case 'stickiness': + case 'groupId': + return null; + default: + return null; + } + }); + }, [parameters, definition, constraints, strategy.disabled]); + + const customStrategyList = useMemo(() => { + if (!parameters || !definition?.editable) return null; + const isSetTo = ( + {' is set to '} + ); + + return definition?.parameters.map((param) => { + const { type, name } = { ...param }; + if (!type || !name || parameters[name] === undefined) { + return null; + } + const nameItem = ( + + ); + + switch (param?.type) { + case 'list': { + const values = parseParameterStrings(parameters[name]); + + return values.length > 0 ? ( + + {nameItem}{' '} + + has {values.length}{' '} + {values.length > 1 ? `items` : 'item'}:{' '} + {values.map((item: string) => ( + + } + sx={{ mr: 0.5 }} + /> + ))} + + + ) : null; + } + + case 'percentage': { + const percentage = parseParameterNumber(parameters[name]); + return parameters[name] !== '' ? ( + + + + +
+ {nameItem} + {isSetTo} + {percentage}% +
+
+ ) : null; + } + + case 'boolean': + return parameters[name] === 'true' || + parameters[name] === 'false' ? ( + + + {isSetTo} + + {parameters[name]} + + + ) : null; + + case 'string': { + const value = parseParameterString(parameters[name]); + return typeof parameters[name] !== 'undefined' ? ( + + {nameItem} + + {' is an empty string'} + + } + elseShow={ + <> + {isSetTo} + + + } + /> + + ) : null; + } + + case 'number': { + const number = parseParameterNumber(parameters[name]); + return parameters[name] !== '' && number !== undefined ? ( + + {nameItem} + {isSetTo} + + + ) : null; + } + case 'default': + return null; + } + + return null; + }); + }, [parameters, definition]); + + if (!parameters) { + return ; + } + + const listItems = [ + strategySegments && strategySegments.length > 0 && ( + + ), + constraints.length > 0 && ( + + ), + strategy.name === 'default' && ( + <> + + The standard strategy is ON{' '} + for all users. + + + ), + ...(parametersList ?? []), + ...(customStrategyList ?? []), + ].filter(Boolean); + + return ( + <> + } + /> + + 0} + show={ + + {listItems.map((item, index) => ( + + 0} + show={} + /> + {item} + + ))} + + } + elseShow={} + /> + + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyEvaluationChip/StrategyEvaluationChip.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyEvaluationChip/StrategyEvaluationChip.tsx new file mode 100644 index 0000000000..0675c37842 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyEvaluationChip/StrategyEvaluationChip.tsx @@ -0,0 +1,18 @@ +import { forwardRef } from 'react'; +import type { ChipProps } from '@mui/material'; +import { Chip, styled } from '@mui/material'; + +const StyledChip = styled(Chip)(({ theme }) => ({ + borderRadius: `${theme.shape.borderRadius}px`, + padding: theme.spacing(0.25, 0), + fontSize: theme.fontSizes.smallerBody, + height: 'auto', + background: theme.palette.secondary.light, + border: `1px solid ${theme.palette.secondary.border}`, + color: theme.palette.secondary.dark, + fontWeight: theme.typography.fontWeightBold, +})); + +export const StrategyEvaluationChip = forwardRef( + (props, ref) => , +); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyEvaluationItem/StrategyEvaluationItem.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyEvaluationItem/StrategyEvaluationItem.tsx new file mode 100644 index 0000000000..61cc5ed7b2 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyEvaluationItem/StrategyEvaluationItem.tsx @@ -0,0 +1,57 @@ +import { Chip, type ChipProps, styled } from '@mui/material'; +import type { FC, ReactNode } from 'react'; + +type StrategyItemProps = { + type?: ReactNode; + children?: ReactNode; + values?: string[]; +}; + +const StyledContainer = styled('div')(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(1), + alignItems: 'center', + fontSize: theme.typography.body2.fontSize, +})); + +const StyledType = styled('span')(({ theme }) => ({ + display: 'block', + fontSize: theme.fontSizes.smallerBody, + fontWeight: theme.typography.fontWeightBold, + color: theme.palette.text.secondary, + width: theme.spacing(10), +})); + +const StyledValuesGroup = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(0.5), +})); + +const StyledValue = styled(({ ...props }: ChipProps) => ( + +))(({ theme }) => ({ + padding: theme.spacing(0.5), + background: theme.palette.background.elevation1, +})); + +/** + * Abstract building block for a list of constraints, segments and other items inside a strategy + */ +export const StrategyEvaluationItem: FC = ({ + type, + children, + values, +}) => ( + + {type} + {children} + {values && values?.length > 0 ? ( + + {values?.map((value, index) => ( + + ))} + + ) : null} + +); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyEvaluationSeparator/StrategyEvaluationSeparator.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyEvaluationSeparator/StrategyEvaluationSeparator.tsx new file mode 100644 index 0000000000..9168f26793 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyEvaluationSeparator/StrategyEvaluationSeparator.tsx @@ -0,0 +1,18 @@ +import { styled } from '@mui/material'; + +const StyledAnd = styled('div')(({ theme }) => ({ + position: 'absolute', + top: theme.spacing(-0.5), + left: theme.spacing(2), + transform: 'translateY(-50%)', + padding: theme.spacing(0.75, 1), + lineHeight: 1, + fontSize: theme.fontSizes.smallerBody, + color: theme.palette.text.primary, + background: theme.palette.background.application, + borderRadius: theme.shape.borderRadiusLarge, +})); + +export const StrategyEvaluationSeparator = () => ( + AND +); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx index 6fc2c3951b..d753a7db35 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx @@ -1,372 +1,110 @@ -import { type FC, Fragment, useMemo } from 'react'; -import { Alert, Box, Chip, Link, styled } from '@mui/material'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle'; -import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator'; -import { ConstraintItem } from './ConstraintItem/ConstraintItem'; -import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; -import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; -import { FeatureOverviewSegment } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment'; -import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList'; -import { - parseParameterNumber, - parseParameterString, - parseParameterStrings, -} from 'utils/parseParameter'; -import StringTruncator from 'component/common/StringTruncator/StringTruncator'; -import { Badge } from 'component/common/Badge/Badge'; +import { Children, isValidElement, type FC, type ReactNode } from 'react'; +import { styled } from '@mui/material'; import type { CreateFeatureStrategySchema } from 'openapi'; import type { IFeatureStrategyPayload } from 'interfaces/strategy'; -import { BuiltInStrategies } from 'utils/strategyNames'; +import { useUiFlag } from 'hooks/useUiFlag'; +import { StrategyExecution as LegacyStrategyExecution } from './LegacyStrategyExecution'; +import { ConstraintItem } from './ConstraintItem/ConstraintItem'; +import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; +import { objectId } from 'utils/objectId'; +import { StrategyEvaluationSeparator } from './StrategyEvaluationSeparator/StrategyEvaluationSeparator'; +import { useCustomStrategyParameters } from './hooks/useCustomStrategyParameters'; +import { useStrategyParameters } from './hooks/useStrategyParameters'; +import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; +import { SegmentItem } from 'component/common/SegmentItem/SegmentItem'; -interface IStrategyExecutionProps { - strategy: IFeatureStrategyPayload | CreateFeatureStrategySchema; - displayGroupId?: boolean; -} +const FilterContainer = styled('div', { + shouldForwardProp: (prop) => prop !== 'grayscale', +})<{ grayscale: boolean }>(({ grayscale }) => + grayscale ? { filter: 'grayscale(1)', opacity: 0.67 } : {}, +); -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 StyledList = styled('ul')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + listStyle: 'none', + padding: 0, + margin: 0, + '&.disabled-strategy': { + filter: 'grayscale(1)', + opacity: 0.67, }, + gap: theme.spacing(1), })); -const CustomStrategyDeprecationWarning = () => ( - - Custom strategies are deprecated and may be removed in a future major - version. Consider rewriting this strategy as a predefined strategy with{' '} - - constraints. - - -); - -const NoItems: FC = () => ( - - This strategy does not have constraints or parameters. - -); - -const StyledValueContainer = styled(Box)(({ theme }) => ({ +const StyledListItem = styled('li')(({ theme }) => ({ + position: 'relative', padding: theme.spacing(2, 3), border: `1px solid ${theme.palette.divider}`, borderRadius: theme.shape.borderRadiusMedium, background: theme.palette.background.default, })); -const StyledValueSeparator = styled('span')(({ theme }) => ({ - color: theme.palette.neutral.main, -})); +const List: FC<{ children: ReactNode }> = ({ children }) => { + const result: ReactNode[] = []; + Children.forEach(children, (child, index) => { + if (isValidElement(child)) { + result.push( + + {index > 0 ? ( + + ) : null} + {child} + , + ); + } + }); -export const StrategyExecution: FC = ({ + return {result}; +}; + +const ListItem: FC<{ children: ReactNode }> = ({ children }) => ( + {children} +); + +type StrategyExecutionProps = { + strategy: IFeatureStrategyPayload | CreateFeatureStrategySchema; + displayGroupId?: boolean; +}; + +export const StrategyExecution: FC = ({ strategy, displayGroupId = false, }) => { - const { parameters, constraints = [] } = strategy; - const stickiness = parameters?.stickiness; - const explainStickiness = - typeof stickiness === 'string' && stickiness !== 'default'; const { strategies } = useStrategies(); const { segments } = useSegments(); - const strategySegments = segments?.filter((segment) => { - return strategy.segments?.includes(segment.id); - }); + const { isCustomStrategy, customStrategyParameters: customStrategyItems } = + useCustomStrategyParameters(strategy, strategies); + const strategyParameters = useStrategyParameters(strategy, displayGroupId); + const { constraints } = strategy; + const strategySegments = segments?.filter((segment) => + strategy.segments?.includes(segment.id), + ); + const flagOverviewRedesign = useUiFlag('flagOverviewRedesign'); - const definition = strategies.find((strategyDefinition) => { - return strategyDefinition.name === strategy.name; - }); - - const parametersList = useMemo(() => { - if (!parameters || definition?.editable) return null; - - return Object.keys(parameters).map((key) => { - switch (key) { - case 'rollout': - case 'Rollout': { - const percentage = parseParameterNumber(parameters[key]); - - const badgeType = strategy.disabled ? 'neutral' : 'success'; - - return ( - - - - -
- {percentage}%{' '} - of your base{' '} - - {explainStickiness ? ( - <> - with {stickiness} - - ) : ( - '' - )}{' '} - - - {constraints.length > 0 - ? 'who match constraints' - : ''}{' '} - is included. - -
- {displayGroupId && parameters.groupId && ( - ({ - ml: 1, - color: theme.palette.info.contrastText, - })} - > - - GroupId: {parameters.groupId} - - - )} -
- ); - } - case 'userIds': - case 'UserIds': { - const users = parseParameterStrings(parameters[key]); - return ( - - ); - } - case 'hostNames': - case 'HostNames': { - const hosts = parseParameterStrings(parameters[key]); - return ( - - ); - } - case 'IPs': { - const IPs = parseParameterStrings(parameters[key]); - return ; - } - case 'stickiness': - case 'groupId': - return null; - default: - return null; - } - }); - }, [parameters, definition, constraints, strategy.disabled]); - - const customStrategyList = useMemo(() => { - if (!parameters || !definition?.editable) return null; - const isSetTo = ( - {' is set to '} + if (!flagOverviewRedesign) { + return ( + ); - - return definition?.parameters.map((param) => { - const { type, name } = { ...param }; - if (!type || !name || parameters[name] === undefined) { - return null; - } - const nameItem = ( - - ); - - switch (param?.type) { - case 'list': { - const values = parseParameterStrings(parameters[name]); - - return values.length > 0 ? ( - - {nameItem}{' '} - - has {values.length}{' '} - {values.length > 1 ? `items` : 'item'}:{' '} - {values.map((item: string) => ( - - } - sx={{ mr: 0.5 }} - /> - ))} - - - ) : null; - } - - case 'percentage': { - const percentage = parseParameterNumber(parameters[name]); - return parameters[name] !== '' ? ( - - - - -
- {nameItem} - {isSetTo} - {percentage}% -
-
- ) : null; - } - - case 'boolean': - return parameters[name] === 'true' || - parameters[name] === 'false' ? ( - - - {isSetTo} - - {parameters[name]} - - - ) : null; - - case 'string': { - const value = parseParameterString(parameters[name]); - return typeof parameters[name] !== 'undefined' ? ( - - {nameItem} - - {' is an empty string'} - - } - elseShow={ - <> - {isSetTo} - - - } - /> - - ) : null; - } - - case 'number': { - const number = parseParameterNumber(parameters[name]); - return parameters[name] !== '' && number !== undefined ? ( - - {nameItem} - {isSetTo} - - - ) : null; - } - case 'default': - return null; - } - - return null; - }); - }, [parameters, definition]); - - if (!parameters) { - return ; } - const listItems = [ - strategySegments && strategySegments.length > 0 && ( - - ), - constraints.length > 0 && ( - - ), - strategy.name === 'default' && ( - <> - - The standard strategy is ON{' '} - for all users. - - - ), - ...(parametersList ?? []), - ...(customStrategyList ?? []), - ].filter(Boolean); - return ( - <> - } - /> - - 0} - show={ - - {listItems.map((item, index) => ( - - 0} - show={} - /> - {item} - - ))} - - } - elseShow={} - /> - + + + {strategySegments?.map((segment) => ( + + ))} + {constraints?.map((constraint, index) => ( + + ))} + {isCustomStrategy ? customStrategyItems : strategyParameters} + + ); }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useCustomStrategyParameters.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useCustomStrategyParameters.tsx new file mode 100644 index 0000000000..202f2c0928 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useCustomStrategyParameters.tsx @@ -0,0 +1,123 @@ +import { useMemo } from 'react'; +import { Truncator } from 'component/common/Truncator/Truncator'; +import { + parseParameterNumber, + parseParameterString, + parseParameterStrings, +} from 'utils/parseParameter'; +import { StrategyEvaluationItem } from '../StrategyEvaluationItem/StrategyEvaluationItem'; +import { StrategyEvaluationChip } from '../StrategyEvaluationChip/StrategyEvaluationChip'; +import type { + CreateFeatureStrategySchema, + StrategySchema, + StrategySchemaParametersItem, +} from 'openapi'; +import type { IFeatureStrategyPayload } from 'interfaces/strategy'; + +export const useCustomStrategyParameters = ( + strategy: CreateFeatureStrategySchema | IFeatureStrategyPayload, + strategies: StrategySchema[], +) => { + const { parameters } = strategy; + const definition = useMemo( + () => + strategies.find((strategyDefinition) => { + return strategyDefinition.name === strategy.name; + }), + [strategies, strategy.name], + ); + const isCustomStrategy = definition?.editable; + + const mapCustomStrategies = ( + param: StrategySchemaParametersItem, + index: number, + ) => { + if (!parameters || !param.name) return null; + const { type, name } = param; + const typeItem = {name}; + const key = `${type}${index}`; + + switch (type) { + case 'list': { + const values = parseParameterStrings(parameters[name]); + if (!values || values.length === 0) { + return null; + } + + return ( + + {values.length === 1 + ? 'has 1 item:' + : `has ${values.length} items:`} + + ); + } + + case 'percentage': { + const value = parseParameterNumber(parameters[name]); + return ( + + is set to + + ); + } + + case 'boolean': { + const value = parameters[name]; + return ( + + is set to + + ); + } + + case 'string': { + const value = parseParameterString(parameters[name]); + + return ( + + {value === '' ? 'is an empty string' : 'is set to'} + + ); + } + + case 'number': { + const value = parseParameterNumber(parameters[name]); + return ( + + is a number set to + + ); + } + + case 'default': + return null; + } + + return null; + }; + + return useMemo( + () => ({ + isCustomStrategy, + customStrategyParameters: isCustomStrategy + ? definition?.parameters + ?.map(mapCustomStrategies) + .filter(Boolean) + : [], + }), + [definition, isCustomStrategy, parameters], + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useStrategyParameters.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useStrategyParameters.tsx new file mode 100644 index 0000000000..966964c834 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useStrategyParameters.tsx @@ -0,0 +1,92 @@ +import { type FC, useMemo } from 'react'; +import { StrategyEvaluationChip } from '../StrategyEvaluationChip/StrategyEvaluationChip'; +import { + parseParameterNumber, + parseParameterStrings, +} from 'utils/parseParameter'; +import { StrategyEvaluationItem } from '../StrategyEvaluationItem/StrategyEvaluationItem'; +import type { IFeatureStrategyPayload } from 'interfaces/strategy'; +import type { CreateFeatureStrategySchema } from 'openapi'; + +const RolloutParameter: FC<{ + value?: string | number; + parameters?: ( + | IFeatureStrategyPayload + | CreateFeatureStrategySchema + )['parameters']; + hasConstraints?: boolean; + displayGroupId?: boolean; +}> = ({ value, parameters, hasConstraints, displayGroupId }) => { + const percentage = parseParameterNumber(value); + + const explainStickiness = + typeof parameters?.stickiness === 'string' && + parameters?.stickiness !== 'default'; + const stickiness = explainStickiness ? ( + <> + with {parameters.stickiness} + + ) : ( + '' + ); + + return ( + + of your base{' '} + {stickiness} + + {hasConstraints ? 'who match constraints ' : ' '} + is included. + + {/* TODO: displayGroupId */} + + ); +}; + +export const useStrategyParameters = ( + strategy: IFeatureStrategyPayload | CreateFeatureStrategySchema, + displayGroupId?: boolean, +) => { + const { constraints } = strategy; + const { parameters } = strategy; + const hasConstraints = Boolean(constraints?.length); + const parameterKeys = parameters ? Object.keys(parameters) : []; + const mapPredefinedStrategies = (key: string) => { + const type = key.toLocaleLowerCase(); + + if (type === 'rollout') { + return ( + + ); + } + + if (['userids', 'hostnames', 'ips'].includes(type)) { + return ( + + ); + } + + return null; + }; + + return useMemo( + () => + [ + ...parameterKeys.map(mapPredefinedStrategies), + strategy.name === 'default' ? ( + + ) : null, + ].filter(Boolean), + [parameters, hasConstraints, displayGroupId], + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx index 0f77b047ed..788354d316 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx @@ -108,6 +108,7 @@ export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => { const isOverflowing = tagLabel.length > 25; return (