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',
|
textDecoration: 'underline',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledText = styled('span', {
|
const StyledText = styled('span', {
|
||||||
shouldForwardProp: (prop) => prop !== 'disabled',
|
shouldForwardProp: (prop) => prop !== 'disabled',
|
||||||
})<{ disabled: boolean | null }>(({ theme, disabled }) => ({
|
})<{ disabled: boolean | null }>(({ theme, disabled }) => ({
|
||||||
|
@ -9,9 +9,7 @@ const Chip = styled('div')(({ theme }) => ({
|
|||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
borderRadius: theme.shape.borderRadiusLarge,
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
fontWeight: 'bold',
|
backgroundColor: theme.palette.secondary.border,
|
||||||
backgroundColor: theme.palette.background.alternative,
|
|
||||||
color: theme.palette.primary.contrastText,
|
|
||||||
left: theme.spacing(4),
|
left: theme.spacing(4),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -1,60 +1,52 @@
|
|||||||
import { Chip, styled } from '@mui/material';
|
import type { FC } from 'react';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { StrategyEvaluationItem } from '../StrategyEvaluationItem/StrategyEvaluationItem';
|
||||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
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 {
|
const Inverted: FC = () => (
|
||||||
value: string[];
|
<Tooltip title='NOT (operator is negated)' arrow>
|
||||||
text: string;
|
<StrategyEvaluationChip label='≠' />
|
||||||
}
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const Operator: FC<{ label: ConstraintSchema['operator'] }> = ({ label }) => (
|
||||||
width: '100%',
|
<Tooltip title={label} arrow>
|
||||||
padding: theme.spacing(2, 3),
|
<StrategyEvaluationChip label={formatOperatorDescription(label)} />
|
||||||
borderRadius: theme.shape.borderRadiusMedium,
|
</Tooltip>
|
||||||
background: theme.palette.background.default,
|
);
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
|
||||||
|
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 }) => ({
|
export const ConstraintItem: FC<ConstraintSchema> = ({
|
||||||
display: 'inline',
|
caseInsensitive,
|
||||||
margin: theme.spacing(0.5, 0),
|
contextName,
|
||||||
maxWidth: '95%',
|
inverted,
|
||||||
textAlign: 'center',
|
operator,
|
||||||
wordBreak: 'break-word',
|
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 (
|
return (
|
||||||
<StyledContainer>
|
<StrategyEvaluationItem type='Constraint' values={items}>
|
||||||
<ConditionallyRender
|
{contextName}
|
||||||
condition={value.length === 0}
|
<StyledOperatorGroup>
|
||||||
show={<p>No {text}s added yet.</p>}
|
{inverted ? <Inverted /> : null}
|
||||||
elseShow={
|
<Operator label={operator} />
|
||||||
<div>
|
{caseInsensitive ? <CaseInsensitive /> : null}
|
||||||
<StyledParagraph>
|
</StyledOperatorGroup>
|
||||||
{value.length}{' '}
|
</StrategyEvaluationItem>
|
||||||
{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,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 { Children, isValidElement, type FC, type ReactNode } from 'react';
|
||||||
import { Alert, Box, Chip, Link, styled } from '@mui/material';
|
import { 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 type { CreateFeatureStrategySchema } from 'openapi';
|
import type { CreateFeatureStrategySchema } from 'openapi';
|
||||||
import type { IFeatureStrategyPayload } from 'interfaces/strategy';
|
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 {
|
const FilterContainer = styled('div', {
|
||||||
strategy: IFeatureStrategyPayload | CreateFeatureStrategySchema;
|
shouldForwardProp: (prop) => prop !== 'grayscale',
|
||||||
displayGroupId?: boolean;
|
})<{ grayscale: boolean }>(({ grayscale }) =>
|
||||||
}
|
grayscale ? { filter: 'grayscale(1)', opacity: 0.67 } : {},
|
||||||
|
);
|
||||||
|
|
||||||
const StyledContainer = styled(Box, {
|
const StyledList = styled('ul')(({ theme }) => ({
|
||||||
shouldForwardProp: (prop) => prop !== 'disabled',
|
display: 'flex',
|
||||||
})<{ disabled?: boolean | null }>(({ theme, disabled }) => ({
|
flexDirection: 'column',
|
||||||
'& p, & span, & h1, & h2, & h3, & h4, & h5, & h6': {
|
listStyle: 'none',
|
||||||
color: disabled ? theme.palette.neutral.main : 'inherit',
|
padding: 0,
|
||||||
},
|
margin: 0,
|
||||||
'.constraint-icon-container': {
|
'&.disabled-strategy': {
|
||||||
backgroundColor: disabled
|
filter: 'grayscale(1)',
|
||||||
? theme.palette.neutral.border
|
opacity: 0.67,
|
||||||
: theme.palette.primary.light,
|
|
||||||
borderRadius: '50%',
|
|
||||||
},
|
|
||||||
'.constraint-icon': {
|
|
||||||
fill: disabled
|
|
||||||
? theme.palette.neutral.light
|
|
||||||
: theme.palette.common.white,
|
|
||||||
},
|
},
|
||||||
|
gap: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const CustomStrategyDeprecationWarning = () => (
|
const StyledListItem = styled('li')(({ theme }) => ({
|
||||||
<Alert severity='warning' sx={{ mb: 2 }}>
|
position: 'relative',
|
||||||
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),
|
padding: theme.spacing(2, 3),
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
borderRadius: theme.shape.borderRadiusMedium,
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
background: theme.palette.background.default,
|
background: theme.palette.background.default,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledValueSeparator = styled('span')(({ theme }) => ({
|
const List: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
color: theme.palette.neutral.main,
|
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,
|
strategy,
|
||||||
displayGroupId = false,
|
displayGroupId = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { parameters, constraints = [] } = strategy;
|
|
||||||
const stickiness = parameters?.stickiness;
|
|
||||||
const explainStickiness =
|
|
||||||
typeof stickiness === 'string' && stickiness !== 'default';
|
|
||||||
const { strategies } = useStrategies();
|
const { strategies } = useStrategies();
|
||||||
const { segments } = useSegments();
|
const { segments } = useSegments();
|
||||||
const strategySegments = segments?.filter((segment) => {
|
const { isCustomStrategy, customStrategyParameters: customStrategyItems } =
|
||||||
return strategy.segments?.includes(segment.id);
|
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) => {
|
if (!flagOverviewRedesign) {
|
||||||
return strategyDefinition.name === strategy.name;
|
return (
|
||||||
});
|
<LegacyStrategyExecution
|
||||||
|
strategy={strategy}
|
||||||
const parametersList = useMemo(() => {
|
displayGroupId={displayGroupId}
|
||||||
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 (
|
return (
|
||||||
<>
|
<FilterContainer grayscale={strategy.disabled === true}>
|
||||||
<ConditionallyRender
|
<List>
|
||||||
condition={
|
{strategySegments?.map((segment) => (
|
||||||
!BuiltInStrategies.includes(strategy.name || 'default')
|
<SegmentItem segment={segment} />
|
||||||
}
|
))}
|
||||||
show={<CustomStrategyDeprecationWarning />}
|
{constraints?.map((constraint, index) => (
|
||||||
/>
|
<ConstraintItem
|
||||||
|
key={`${objectId(constraint)}-${index}`}
|
||||||
<ConditionallyRender
|
{...constraint}
|
||||||
condition={listItems.length > 0}
|
/>
|
||||||
show={
|
))}
|
||||||
<StyledContainer disabled={Boolean(strategy.disabled)}>
|
{isCustomStrategy ? customStrategyItems : strategyParameters}
|
||||||
{listItems.map((item, index) => (
|
</List>
|
||||||
<Fragment key={index}>
|
</FilterContainer>
|
||||||
<ConditionallyRender
|
|
||||||
condition={index > 0}
|
|
||||||
show={<StrategySeparator text='AND' />}
|
|
||||||
/>
|
|
||||||
{item}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</StyledContainer>
|
|
||||||
}
|
|
||||||
elseShow={<NoItems />}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
const isOverflowing = tagLabel.length > 25;
|
||||||
return (
|
return (
|
||||||
<StyledTag
|
<StyledTag
|
||||||
|
key={tagLabel}
|
||||||
label={
|
label={
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={tagLabel}
|
key={tagLabel}
|
||||||
|
Loading…
Reference in New Issue
Block a user