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 (