mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-18 01:18:23 +02:00
refactor: new constraints style (#9363)
Refactored styles for strategy evaluation parameters. New look for constraints etc
This commit is contained in:
parent
97fd1c0fec
commit
a6cfcea029
@ -56,6 +56,7 @@ const StyledLink = styled(Link)(({ theme }) => ({
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledText = styled('span', {
|
||||
shouldForwardProp: (prop) => prop !== 'disabled',
|
||||
})<{ disabled: boolean | null }>(({ theme, disabled }) => ({
|
||||
|
@ -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),
|
||||
}));
|
||||
|
||||
|
@ -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 = () => (
|
||||
<Tooltip title='NOT (operator is negated)' arrow>
|
||||
<StrategyEvaluationChip label='≠' />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
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 }) => (
|
||||
<Tooltip title={label} arrow>
|
||||
<StrategyEvaluationChip label={formatOperatorDescription(label)} />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const CaseInsensitive: FC = () => (
|
||||
<Tooltip title='Case sensitive' arrow>
|
||||
<StrategyEvaluationChip label={<s>Aa</s>} />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
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<ConstraintSchema> = ({
|
||||
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 (
|
||||
<StyledContainer>
|
||||
<ConditionallyRender
|
||||
condition={value.length === 0}
|
||||
show={<p>No {text}s added yet.</p>}
|
||||
elseShow={
|
||||
<div>
|
||||
<StyledParagraph>
|
||||
{value.length}{' '}
|
||||
{value.length > 1 ? `${text}s` : text} will get
|
||||
access.
|
||||
</StyledParagraph>
|
||||
{value.map((v: string) => (
|
||||
<StyledChip
|
||||
key={v}
|
||||
label={
|
||||
<StringTruncator
|
||||
maxWidth='300'
|
||||
text={v}
|
||||
maxLength={50}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</StyledContainer>
|
||||
<StrategyEvaluationItem type='Constraint' values={items}>
|
||||
{contextName}
|
||||
<StyledOperatorGroup>
|
||||
{inverted ? <Inverted /> : null}
|
||||
<Operator label={operator} />
|
||||
{caseInsensitive ? <CaseInsensitive /> : null}
|
||||
</StyledOperatorGroup>
|
||||
</StrategyEvaluationItem>
|
||||
);
|
||||
};
|
||||
|
@ -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 (
|
||||
<StyledContainer>
|
||||
<ConditionallyRender
|
||||
condition={value.length === 0}
|
||||
show={<p>No {text}s added yet.</p>}
|
||||
elseShow={
|
||||
<div>
|
||||
<StyledParagraph>
|
||||
{value.length}{' '}
|
||||
{value.length > 1 ? `${text}s` : text} will get
|
||||
access.
|
||||
</StyledParagraph>
|
||||
{value.map((v: string) => (
|
||||
<StyledChip
|
||||
key={v}
|
||||
label={
|
||||
<StringTruncator
|
||||
maxWidth='300'
|
||||
text={v}
|
||||
maxLength={50}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -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 = () => (
|
||||
<Alert severity='warning' sx={{ mb: 2 }}>
|
||||
Custom strategies are deprecated and may be removed in a future major
|
||||
version. Consider rewriting this strategy as a predefined strategy with{' '}
|
||||
<Link
|
||||
href={
|
||||
'https://docs.getunleash.io/reference/activation-strategies#constraints'
|
||||
}
|
||||
target='_blank'
|
||||
variant='body2'
|
||||
>
|
||||
constraints.
|
||||
</Link>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
const NoItems: FC = () => (
|
||||
<Box sx={{ px: 3, color: 'text.disabled' }}>
|
||||
This strategy does not have constraints or parameters.
|
||||
</Box>
|
||||
);
|
||||
|
||||
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<IStrategyExecutionProps> = ({
|
||||
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 (
|
||||
<StyledValueContainer
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<Box sx={{ mr: 2 }}>
|
||||
<PercentageCircle
|
||||
percentage={percentage}
|
||||
size='2rem'
|
||||
disabled={strategy.disabled}
|
||||
/>
|
||||
</Box>
|
||||
<div>
|
||||
<Badge color={badgeType}>{percentage}%</Badge>{' '}
|
||||
<span>of your base</span>{' '}
|
||||
<span>
|
||||
{explainStickiness ? (
|
||||
<>
|
||||
with <strong>{stickiness}</strong>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}{' '}
|
||||
</span>
|
||||
<span>
|
||||
{constraints.length > 0
|
||||
? 'who match constraints'
|
||||
: ''}{' '}
|
||||
is included.
|
||||
</span>
|
||||
</div>
|
||||
{displayGroupId && parameters.groupId && (
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
ml: 1,
|
||||
color: theme.palette.info.contrastText,
|
||||
})}
|
||||
>
|
||||
<Badge color='info'>
|
||||
GroupId: {parameters.groupId}
|
||||
</Badge>
|
||||
</Box>
|
||||
)}
|
||||
</StyledValueContainer>
|
||||
);
|
||||
}
|
||||
case 'userIds':
|
||||
case 'UserIds': {
|
||||
const users = parseParameterStrings(parameters[key]);
|
||||
return (
|
||||
<ConstraintItem key={key} value={users} text='user' />
|
||||
);
|
||||
}
|
||||
case 'hostNames':
|
||||
case 'HostNames': {
|
||||
const hosts = parseParameterStrings(parameters[key]);
|
||||
return (
|
||||
<ConstraintItem key={key} value={hosts} text={'host'} />
|
||||
);
|
||||
}
|
||||
case 'IPs': {
|
||||
const IPs = parseParameterStrings(parameters[key]);
|
||||
return <ConstraintItem key={key} value={IPs} text={'IP'} />;
|
||||
}
|
||||
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 = (
|
||||
<StyledValueSeparator>{' is set to '}</StyledValueSeparator>
|
||||
);
|
||||
|
||||
return definition?.parameters.map((param) => {
|
||||
const { type, name } = { ...param };
|
||||
if (!type || !name || parameters[name] === undefined) {
|
||||
return null;
|
||||
}
|
||||
const nameItem = (
|
||||
<StringTruncator maxLength={15} maxWidth='150' text={name} />
|
||||
);
|
||||
|
||||
switch (param?.type) {
|
||||
case 'list': {
|
||||
const values = parseParameterStrings(parameters[name]);
|
||||
|
||||
return values.length > 0 ? (
|
||||
<StyledValueContainer>
|
||||
{nameItem}{' '}
|
||||
<StyledValueSeparator>
|
||||
has {values.length}{' '}
|
||||
{values.length > 1 ? `items` : 'item'}:{' '}
|
||||
{values.map((item: string) => (
|
||||
<Chip
|
||||
key={item}
|
||||
label={
|
||||
<StringTruncator
|
||||
maxWidth='300'
|
||||
text={item}
|
||||
maxLength={50}
|
||||
/>
|
||||
}
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
))}
|
||||
</StyledValueSeparator>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
}
|
||||
|
||||
case 'percentage': {
|
||||
const percentage = parseParameterNumber(parameters[name]);
|
||||
return parameters[name] !== '' ? (
|
||||
<StyledValueContainer
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<Box sx={{ mr: 2 }}>
|
||||
<PercentageCircle
|
||||
percentage={percentage}
|
||||
size='2rem'
|
||||
/>
|
||||
</Box>
|
||||
<div>
|
||||
{nameItem}
|
||||
{isSetTo}
|
||||
<Badge color='success'>{percentage}%</Badge>
|
||||
</div>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
}
|
||||
|
||||
case 'boolean':
|
||||
return parameters[name] === 'true' ||
|
||||
parameters[name] === 'false' ? (
|
||||
<StyledValueContainer>
|
||||
<StringTruncator
|
||||
maxLength={15}
|
||||
maxWidth='150'
|
||||
text={name}
|
||||
/>
|
||||
{isSetTo}
|
||||
<Badge
|
||||
color={
|
||||
parameters[name] === 'true'
|
||||
? 'success'
|
||||
: 'error'
|
||||
}
|
||||
>
|
||||
{parameters[name]}
|
||||
</Badge>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
|
||||
case 'string': {
|
||||
const value = parseParameterString(parameters[name]);
|
||||
return typeof parameters[name] !== 'undefined' ? (
|
||||
<StyledValueContainer>
|
||||
{nameItem}
|
||||
<ConditionallyRender
|
||||
condition={value === ''}
|
||||
show={
|
||||
<StyledValueSeparator>
|
||||
{' is an empty string'}
|
||||
</StyledValueSeparator>
|
||||
}
|
||||
elseShow={
|
||||
<>
|
||||
{isSetTo}
|
||||
<StringTruncator
|
||||
maxWidth='300'
|
||||
text={value}
|
||||
maxLength={50}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
const number = parseParameterNumber(parameters[name]);
|
||||
return parameters[name] !== '' && number !== undefined ? (
|
||||
<StyledValueContainer>
|
||||
{nameItem}
|
||||
{isSetTo}
|
||||
<StringTruncator
|
||||
maxWidth='300'
|
||||
text={String(number)}
|
||||
maxLength={50}
|
||||
/>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
}
|
||||
case 'default':
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}, [parameters, definition]);
|
||||
|
||||
if (!parameters) {
|
||||
return <NoItems />;
|
||||
}
|
||||
|
||||
const listItems = [
|
||||
strategySegments && strategySegments.length > 0 && (
|
||||
<FeatureOverviewSegment
|
||||
segments={strategySegments}
|
||||
disabled={strategy.disabled}
|
||||
/>
|
||||
),
|
||||
constraints.length > 0 && (
|
||||
<ConstraintAccordionList
|
||||
constraints={constraints}
|
||||
showLabel={false}
|
||||
/>
|
||||
),
|
||||
strategy.name === 'default' && (
|
||||
<>
|
||||
<StyledValueContainer sx={{ width: '100%' }}>
|
||||
The standard strategy is <Badge color='success'>ON</Badge>{' '}
|
||||
for all users.
|
||||
</StyledValueContainer>
|
||||
</>
|
||||
),
|
||||
...(parametersList ?? []),
|
||||
...(customStrategyList ?? []),
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
!BuiltInStrategies.includes(strategy.name || 'default')
|
||||
}
|
||||
show={<CustomStrategyDeprecationWarning />}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={listItems.length > 0}
|
||||
show={
|
||||
<StyledContainer disabled={Boolean(strategy.disabled)}>
|
||||
{listItems.map((item, index) => (
|
||||
<Fragment key={index}>
|
||||
<ConditionallyRender
|
||||
condition={index > 0}
|
||||
show={<StrategySeparator text='AND' />}
|
||||
/>
|
||||
{item}
|
||||
</Fragment>
|
||||
))}
|
||||
</StyledContainer>
|
||||
}
|
||||
elseShow={<NoItems />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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<HTMLDivElement, ChipProps>(
|
||||
(props, ref) => <StyledChip size='small' ref={ref} {...props} />,
|
||||
);
|
@ -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) => (
|
||||
<Chip size='small' {...props} />
|
||||
))(({ 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<StrategyItemProps> = ({
|
||||
type,
|
||||
children,
|
||||
values,
|
||||
}) => (
|
||||
<StyledContainer>
|
||||
<StyledType>{type}</StyledType>
|
||||
{children}
|
||||
{values && values?.length > 0 ? (
|
||||
<StyledValuesGroup>
|
||||
{values?.map((value, index) => (
|
||||
<StyledValue key={`${value}#${index}`} label={value} />
|
||||
))}
|
||||
</StyledValuesGroup>
|
||||
) : null}
|
||||
</StyledContainer>
|
||||
);
|
@ -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 = () => (
|
||||
<StyledAnd role='separator'>AND</StyledAnd>
|
||||
);
|
@ -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 = () => (
|
||||
<Alert severity='warning' sx={{ mb: 2 }}>
|
||||
Custom strategies are deprecated and may be removed in a future major
|
||||
version. Consider rewriting this strategy as a predefined strategy with{' '}
|
||||
<Link
|
||||
href={
|
||||
'https://docs.getunleash.io/reference/activation-strategies#constraints'
|
||||
}
|
||||
target='_blank'
|
||||
variant='body2'
|
||||
>
|
||||
constraints.
|
||||
</Link>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
const NoItems: FC = () => (
|
||||
<Box sx={{ px: 3, color: 'text.disabled' }}>
|
||||
This strategy does not have constraints or parameters.
|
||||
</Box>
|
||||
);
|
||||
|
||||
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(
|
||||
<ListItem key={index}>
|
||||
{index > 0 ? (
|
||||
<StrategyEvaluationSeparator key={`${index}-divider`} />
|
||||
) : null}
|
||||
{child}
|
||||
</ListItem>,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export const StrategyExecution: FC<IStrategyExecutionProps> = ({
|
||||
return <StyledList>{result}</StyledList>;
|
||||
};
|
||||
|
||||
const ListItem: FC<{ children: ReactNode }> = ({ children }) => (
|
||||
<StyledListItem>{children}</StyledListItem>
|
||||
);
|
||||
|
||||
type StrategyExecutionProps = {
|
||||
strategy: IFeatureStrategyPayload | CreateFeatureStrategySchema;
|
||||
displayGroupId?: boolean;
|
||||
};
|
||||
|
||||
export const StrategyExecution: FC<StrategyExecutionProps> = ({
|
||||
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 (
|
||||
<StyledValueContainer
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<Box sx={{ mr: 2 }}>
|
||||
<PercentageCircle
|
||||
percentage={percentage}
|
||||
size='2rem'
|
||||
disabled={strategy.disabled}
|
||||
/>
|
||||
</Box>
|
||||
<div>
|
||||
<Badge color={badgeType}>{percentage}%</Badge>{' '}
|
||||
<span>of your base</span>{' '}
|
||||
<span>
|
||||
{explainStickiness ? (
|
||||
<>
|
||||
with <strong>{stickiness}</strong>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}{' '}
|
||||
</span>
|
||||
<span>
|
||||
{constraints.length > 0
|
||||
? 'who match constraints'
|
||||
: ''}{' '}
|
||||
is included.
|
||||
</span>
|
||||
</div>
|
||||
{displayGroupId && parameters.groupId && (
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
ml: 1,
|
||||
color: theme.palette.info.contrastText,
|
||||
})}
|
||||
>
|
||||
<Badge color='info'>
|
||||
GroupId: {parameters.groupId}
|
||||
</Badge>
|
||||
</Box>
|
||||
)}
|
||||
</StyledValueContainer>
|
||||
);
|
||||
}
|
||||
case 'userIds':
|
||||
case 'UserIds': {
|
||||
const users = parseParameterStrings(parameters[key]);
|
||||
return (
|
||||
<ConstraintItem key={key} value={users} text='user' />
|
||||
);
|
||||
}
|
||||
case 'hostNames':
|
||||
case 'HostNames': {
|
||||
const hosts = parseParameterStrings(parameters[key]);
|
||||
return (
|
||||
<ConstraintItem key={key} value={hosts} text={'host'} />
|
||||
);
|
||||
}
|
||||
case 'IPs': {
|
||||
const IPs = parseParameterStrings(parameters[key]);
|
||||
return <ConstraintItem key={key} value={IPs} text={'IP'} />;
|
||||
}
|
||||
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 = (
|
||||
<StyledValueSeparator>{' is set to '}</StyledValueSeparator>
|
||||
if (!flagOverviewRedesign) {
|
||||
return (
|
||||
<LegacyStrategyExecution
|
||||
strategy={strategy}
|
||||
displayGroupId={displayGroupId}
|
||||
/>
|
||||
);
|
||||
|
||||
return definition?.parameters.map((param) => {
|
||||
const { type, name } = { ...param };
|
||||
if (!type || !name || parameters[name] === undefined) {
|
||||
return null;
|
||||
}
|
||||
const nameItem = (
|
||||
<StringTruncator maxLength={15} maxWidth='150' text={name} />
|
||||
);
|
||||
|
||||
switch (param?.type) {
|
||||
case 'list': {
|
||||
const values = parseParameterStrings(parameters[name]);
|
||||
|
||||
return values.length > 0 ? (
|
||||
<StyledValueContainer>
|
||||
{nameItem}{' '}
|
||||
<StyledValueSeparator>
|
||||
has {values.length}{' '}
|
||||
{values.length > 1 ? `items` : 'item'}:{' '}
|
||||
{values.map((item: string) => (
|
||||
<Chip
|
||||
key={item}
|
||||
label={
|
||||
<StringTruncator
|
||||
maxWidth='300'
|
||||
text={item}
|
||||
maxLength={50}
|
||||
/>
|
||||
}
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
))}
|
||||
</StyledValueSeparator>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
}
|
||||
|
||||
case 'percentage': {
|
||||
const percentage = parseParameterNumber(parameters[name]);
|
||||
return parameters[name] !== '' ? (
|
||||
<StyledValueContainer
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<Box sx={{ mr: 2 }}>
|
||||
<PercentageCircle
|
||||
percentage={percentage}
|
||||
size='2rem'
|
||||
/>
|
||||
</Box>
|
||||
<div>
|
||||
{nameItem}
|
||||
{isSetTo}
|
||||
<Badge color='success'>{percentage}%</Badge>
|
||||
</div>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
}
|
||||
|
||||
case 'boolean':
|
||||
return parameters[name] === 'true' ||
|
||||
parameters[name] === 'false' ? (
|
||||
<StyledValueContainer>
|
||||
<StringTruncator
|
||||
maxLength={15}
|
||||
maxWidth='150'
|
||||
text={name}
|
||||
/>
|
||||
{isSetTo}
|
||||
<Badge
|
||||
color={
|
||||
parameters[name] === 'true'
|
||||
? 'success'
|
||||
: 'error'
|
||||
}
|
||||
>
|
||||
{parameters[name]}
|
||||
</Badge>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
|
||||
case 'string': {
|
||||
const value = parseParameterString(parameters[name]);
|
||||
return typeof parameters[name] !== 'undefined' ? (
|
||||
<StyledValueContainer>
|
||||
{nameItem}
|
||||
<ConditionallyRender
|
||||
condition={value === ''}
|
||||
show={
|
||||
<StyledValueSeparator>
|
||||
{' is an empty string'}
|
||||
</StyledValueSeparator>
|
||||
}
|
||||
elseShow={
|
||||
<>
|
||||
{isSetTo}
|
||||
<StringTruncator
|
||||
maxWidth='300'
|
||||
text={value}
|
||||
maxLength={50}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
const number = parseParameterNumber(parameters[name]);
|
||||
return parameters[name] !== '' && number !== undefined ? (
|
||||
<StyledValueContainer>
|
||||
{nameItem}
|
||||
{isSetTo}
|
||||
<StringTruncator
|
||||
maxWidth='300'
|
||||
text={String(number)}
|
||||
maxLength={50}
|
||||
/>
|
||||
</StyledValueContainer>
|
||||
) : null;
|
||||
}
|
||||
case 'default':
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}, [parameters, definition]);
|
||||
|
||||
if (!parameters) {
|
||||
return <NoItems />;
|
||||
}
|
||||
|
||||
const listItems = [
|
||||
strategySegments && strategySegments.length > 0 && (
|
||||
<FeatureOverviewSegment
|
||||
segments={strategySegments}
|
||||
disabled={strategy.disabled}
|
||||
/>
|
||||
),
|
||||
constraints.length > 0 && (
|
||||
<ConstraintAccordionList
|
||||
constraints={constraints}
|
||||
showLabel={false}
|
||||
/>
|
||||
),
|
||||
strategy.name === 'default' && (
|
||||
<>
|
||||
<StyledValueContainer sx={{ width: '100%' }}>
|
||||
The standard strategy is <Badge color='success'>ON</Badge>{' '}
|
||||
for all users.
|
||||
</StyledValueContainer>
|
||||
</>
|
||||
),
|
||||
...(parametersList ?? []),
|
||||
...(customStrategyList ?? []),
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
!BuiltInStrategies.includes(strategy.name || 'default')
|
||||
}
|
||||
show={<CustomStrategyDeprecationWarning />}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={listItems.length > 0}
|
||||
show={
|
||||
<StyledContainer disabled={Boolean(strategy.disabled)}>
|
||||
{listItems.map((item, index) => (
|
||||
<Fragment key={index}>
|
||||
<ConditionallyRender
|
||||
condition={index > 0}
|
||||
show={<StrategySeparator text='AND' />}
|
||||
/>
|
||||
{item}
|
||||
</Fragment>
|
||||
))}
|
||||
</StyledContainer>
|
||||
}
|
||||
elseShow={<NoItems />}
|
||||
/>
|
||||
</>
|
||||
<FilterContainer grayscale={strategy.disabled === true}>
|
||||
<List>
|
||||
{strategySegments?.map((segment) => (
|
||||
<SegmentItem segment={segment} />
|
||||
))}
|
||||
{constraints?.map((constraint, index) => (
|
||||
<ConstraintItem
|
||||
key={`${objectId(constraint)}-${index}`}
|
||||
{...constraint}
|
||||
/>
|
||||
))}
|
||||
{isCustomStrategy ? customStrategyItems : strategyParameters}
|
||||
</List>
|
||||
</FilterContainer>
|
||||
);
|
||||
};
|
||||
|
@ -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 = <Truncator title={name}>{name}</Truncator>;
|
||||
const key = `${type}${index}`;
|
||||
|
||||
switch (type) {
|
||||
case 'list': {
|
||||
const values = parseParameterStrings(parameters[name]);
|
||||
if (!values || values.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StrategyEvaluationItem
|
||||
key={key}
|
||||
type={typeItem}
|
||||
values={values}
|
||||
>
|
||||
{values.length === 1
|
||||
? 'has 1 item:'
|
||||
: `has ${values.length} items:`}
|
||||
</StrategyEvaluationItem>
|
||||
);
|
||||
}
|
||||
|
||||
case 'percentage': {
|
||||
const value = parseParameterNumber(parameters[name]);
|
||||
return (
|
||||
<StrategyEvaluationItem key={key} type={typeItem}>
|
||||
is set to <StrategyEvaluationChip label={`${value}%`} />
|
||||
</StrategyEvaluationItem>
|
||||
);
|
||||
}
|
||||
|
||||
case 'boolean': {
|
||||
const value = parameters[name];
|
||||
return (
|
||||
<StrategyEvaluationItem key={key} type={typeItem}>
|
||||
is set to <StrategyEvaluationChip label={value} />
|
||||
</StrategyEvaluationItem>
|
||||
);
|
||||
}
|
||||
|
||||
case 'string': {
|
||||
const value = parseParameterString(parameters[name]);
|
||||
|
||||
return (
|
||||
<StrategyEvaluationItem
|
||||
key={key}
|
||||
type={typeItem}
|
||||
values={value === '' ? undefined : [value]}
|
||||
>
|
||||
{value === '' ? 'is an empty string' : 'is set to'}
|
||||
</StrategyEvaluationItem>
|
||||
);
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
const value = parseParameterNumber(parameters[name]);
|
||||
return (
|
||||
<StrategyEvaluationItem
|
||||
key={key}
|
||||
type={typeItem}
|
||||
values={[`${value}`]}
|
||||
>
|
||||
is a number set to
|
||||
</StrategyEvaluationItem>
|
||||
);
|
||||
}
|
||||
|
||||
case 'default':
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
isCustomStrategy,
|
||||
customStrategyParameters: isCustomStrategy
|
||||
? definition?.parameters
|
||||
?.map(mapCustomStrategies)
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
}),
|
||||
[definition, isCustomStrategy, parameters],
|
||||
);
|
||||
};
|
@ -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 <strong>{parameters.stickiness}</strong>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
|
||||
return (
|
||||
<StrategyEvaluationItem type='Rollout %'>
|
||||
<StrategyEvaluationChip label={`${percentage}%`} /> of your base{' '}
|
||||
{stickiness}
|
||||
<span>
|
||||
{hasConstraints ? 'who match constraints ' : ' '}
|
||||
is included.
|
||||
</span>
|
||||
{/* TODO: displayGroupId */}
|
||||
</StrategyEvaluationItem>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<RolloutParameter
|
||||
key={key}
|
||||
value={parameters?.[key]}
|
||||
parameters={parameters}
|
||||
hasConstraints={hasConstraints}
|
||||
displayGroupId={displayGroupId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (['userids', 'hostnames', 'ips'].includes(type)) {
|
||||
return (
|
||||
<StrategyEvaluationItem
|
||||
key={key}
|
||||
type={key}
|
||||
values={parseParameterStrings(parameters?.[key])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
[
|
||||
...parameterKeys.map(mapPredefinedStrategies),
|
||||
strategy.name === 'default' ? (
|
||||
<RolloutParameter value={100} />
|
||||
) : null,
|
||||
].filter(Boolean),
|
||||
[parameters, hasConstraints, displayGroupId],
|
||||
);
|
||||
};
|
@ -108,6 +108,7 @@ export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => {
|
||||
const isOverflowing = tagLabel.length > 25;
|
||||
return (
|
||||
<StyledTag
|
||||
key={tagLabel}
|
||||
label={
|
||||
<Tooltip
|
||||
key={tagLabel}
|
||||
|
Loading…
Reference in New Issue
Block a user