mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
Feat: show disabled strategies in playground (#5081)
Show disabled strategies in separate list Do not show any results in segments/constraints when disabled Closes # [1-1506](https://linear.app/unleash/issue/1-1506/display-disabled-strategies-in-playground-page)  --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
parent
ba53d392b2
commit
1335da6366
@ -3,6 +3,7 @@ import { render } from 'utils/testRenderer';
|
||||
import React from 'react';
|
||||
import { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
|
||||
import { PlaygroundResultFeatureStrategyList } from './PlaygroundResultFeatureStrategyList';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
@ -62,8 +63,72 @@ const testCases = [
|
||||
expectedText:
|
||||
'If environment was enabled, then this feature toggle would be TRUE with strategies evaluated like so:',
|
||||
},
|
||||
{
|
||||
name: 'Has disabled strategies and is enabled in environment',
|
||||
feature: {
|
||||
strategies: {
|
||||
result: true,
|
||||
data: [
|
||||
{
|
||||
name: 'default',
|
||||
parameters: {},
|
||||
result: { enabled: true, evaluationStatus: 'complete' },
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
parameters: {},
|
||||
disabled: true,
|
||||
result: {
|
||||
enabled: 'unknown',
|
||||
evaluationStatus: 'unevaluated',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
isEnabledInCurrentEnvironment: true,
|
||||
hasUnsatisfiedDependency: false,
|
||||
} as PlaygroundFeatureSchema,
|
||||
expectedText:
|
||||
'Disabled strategies are not evaluated for the overall result.',
|
||||
},
|
||||
{
|
||||
name: 'Has disabled strategies and is disabled in environment',
|
||||
feature: {
|
||||
strategies: {
|
||||
result: true,
|
||||
data: [
|
||||
{
|
||||
name: 'default',
|
||||
parameters: {},
|
||||
result: { enabled: true, evaluationStatus: 'complete' },
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
parameters: {},
|
||||
disabled: true,
|
||||
result: {
|
||||
enabled: 'unknown',
|
||||
evaluationStatus: 'unevaluated',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
isEnabledInCurrentEnvironment: false,
|
||||
hasUnsatisfiedDependency: false,
|
||||
} as PlaygroundFeatureSchema,
|
||||
expectedText:
|
||||
'Disabled strategies are not evaluated for the overall result.',
|
||||
},
|
||||
];
|
||||
|
||||
vi.mock('../../../../../../hooks/useUiFlag', () => ({
|
||||
useUiFlag: vi.fn().mockImplementation(() => true),
|
||||
}));
|
||||
|
||||
afterAll(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
testCases.forEach(({ name, feature, expectedText }) => {
|
||||
test(name, async () => {
|
||||
render(
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
|
||||
import { Alert } from '@mui/material';
|
||||
import { useUiFlag } from '../../../../../../hooks/useUiFlag';
|
||||
|
||||
interface PlaygroundResultFeatureStrategyListProps {
|
||||
feature: PlaygroundFeatureSchema;
|
||||
@ -15,6 +16,17 @@ export const PlaygroundResultFeatureStrategyList = ({
|
||||
feature,
|
||||
input,
|
||||
}: PlaygroundResultFeatureStrategyListProps) => {
|
||||
const playgroundImprovements = useUiFlag('playgroundImprovements');
|
||||
const enabledStrategies = feature.strategies?.data?.filter(
|
||||
(strategy) => !strategy.disabled,
|
||||
);
|
||||
const disabledStrategies = feature.strategies?.data?.filter(
|
||||
(strategy) => strategy.disabled,
|
||||
);
|
||||
|
||||
const showDisabledStrategies =
|
||||
playgroundImprovements && disabledStrategies?.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConditionallyRender
|
||||
@ -39,10 +51,28 @@ export const PlaygroundResultFeatureStrategyList = ({
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<>
|
||||
<PlaygroundResultStrategyLists
|
||||
strategies={feature?.strategies?.data || []}
|
||||
strategies={enabledStrategies || []}
|
||||
input={input}
|
||||
titlePrefix={
|
||||
showDisabledStrategies ? 'Enabled' : ''
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={showDisabledStrategies}
|
||||
show={
|
||||
<PlaygroundResultStrategyLists
|
||||
strategies={disabledStrategies}
|
||||
input={input}
|
||||
titlePrefix={'Disabled'}
|
||||
infoText={
|
||||
'Disabled strategies are not evaluated for the overall result.'
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
|
@ -4,6 +4,8 @@ import { PlaygroundStrategySchema, PlaygroundRequestSchema } from 'openapi';
|
||||
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
|
||||
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
|
||||
import { objectId } from 'utils/objectId';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { DisabledStrategyExecution } from './StrategyExecution/DisabledStrategyExecution';
|
||||
|
||||
interface IFeatureStrategyItemProps {
|
||||
strategy: PlaygroundStrategySchema;
|
||||
@ -19,7 +21,8 @@ export const FeatureStrategyItem = ({
|
||||
const { result } = strategy;
|
||||
const theme = useTheme();
|
||||
const label =
|
||||
result.evaluationStatus === 'incomplete'
|
||||
result.evaluationStatus === 'incomplete' ||
|
||||
result.evaluationStatus === 'unevaluated'
|
||||
? 'Unevaluated'
|
||||
: result.enabled
|
||||
? 'True'
|
||||
@ -28,7 +31,8 @@ export const FeatureStrategyItem = ({
|
||||
return (
|
||||
<StrategyItemContainer
|
||||
style={{
|
||||
borderColor: result.enabled
|
||||
borderColor:
|
||||
result.enabled && result.evaluationStatus === 'complete'
|
||||
? theme.palette.success.main
|
||||
: 'none',
|
||||
}}
|
||||
@ -42,11 +46,22 @@ export const FeatureStrategyItem = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(strategy.disabled)}
|
||||
show={
|
||||
<DisabledStrategyExecution
|
||||
strategyResult={strategy}
|
||||
input={input}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<StrategyExecution
|
||||
strategyResult={strategy}
|
||||
input={input}
|
||||
percentageFill={theme.palette.background.elevation2}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StrategyItemContainer>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,43 @@
|
||||
import { Fragment, VFC } from 'react';
|
||||
import {
|
||||
PlaygroundConstraintSchema,
|
||||
PlaygroundRequestSchema,
|
||||
PlaygroundStrategySchemaResultAnyOfEvaluationStatus,
|
||||
} from 'openapi';
|
||||
import { objectId } from 'utils/objectId';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||
import { styled } from '@mui/material';
|
||||
import { ConstraintAccordionView } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
|
||||
import { ConstraintError } from './ConstraintError/ConstraintError';
|
||||
import { ConstraintOk } from './ConstraintOk/ConstraintOk';
|
||||
|
||||
interface IConstraintExecutionWithoutResultsProps {
|
||||
constraints?: PlaygroundConstraintSchema[];
|
||||
}
|
||||
|
||||
export const ConstraintExecutionWrapper = styled('div')(() => ({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}));
|
||||
|
||||
export const ConstraintExecutionWithoutResults: VFC<
|
||||
IConstraintExecutionWithoutResultsProps
|
||||
> = ({ constraints }) => {
|
||||
if (!constraints) return null;
|
||||
|
||||
return (
|
||||
<ConstraintExecutionWrapper>
|
||||
{constraints?.map((constraint, index) => (
|
||||
<Fragment key={objectId(constraint)}>
|
||||
<ConditionallyRender
|
||||
condition={index > 0}
|
||||
show={<StrategySeparator text='AND' />}
|
||||
/>
|
||||
<ConstraintAccordionView constraint={constraint} compact />
|
||||
</Fragment>
|
||||
))}
|
||||
</ConstraintExecutionWrapper>
|
||||
);
|
||||
};
|
@ -0,0 +1,85 @@
|
||||
import { Fragment, VFC } from 'react';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||
import { styled } from '@mui/material';
|
||||
import { PlaygroundRequestSchema, PlaygroundStrategySchema } from 'openapi';
|
||||
import { ConstraintExecution } from './ConstraintExecution/ConstraintExecution';
|
||||
import { SegmentExecution } from './SegmentExecution/SegmentExecution';
|
||||
import { PlaygroundResultStrategyExecutionParameters } from './StrategyExecutionParameters/StrategyExecutionParameters';
|
||||
import { CustomStrategyParams } from './CustomStrategyParams/CustomStrategyParams';
|
||||
import { formattedStrategyNames } from 'utils/strategyNames';
|
||||
import { StyledBoxSummary } from './StrategyExecution.styles';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import { ConstraintExecutionWithoutResults } from './ConstraintExecution/ConstraintExecutionWithoutResults';
|
||||
import { SegmentExecutionWithoutResult } from './SegmentExecution/SegmentExecutionWithoutResult';
|
||||
|
||||
interface IDisabledStrategyExecutionProps {
|
||||
strategyResult: PlaygroundStrategySchema;
|
||||
percentageFill?: string;
|
||||
input?: PlaygroundRequestSchema;
|
||||
}
|
||||
|
||||
const StyledStrategyExecutionWrapper = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(0),
|
||||
}));
|
||||
|
||||
export const DisabledStrategyExecution: VFC<IDisabledStrategyExecutionProps> =
|
||||
({ strategyResult, input, percentageFill }) => {
|
||||
const { name, constraints, segments, parameters } = strategyResult;
|
||||
|
||||
const hasSegments = Boolean(segments && segments.length > 0);
|
||||
const hasConstraints = Boolean(constraints && constraints?.length > 0);
|
||||
const hasExecutionParameters =
|
||||
name !== 'default' &&
|
||||
Object.keys(formattedStrategyNames).includes(name);
|
||||
const hasCustomStrategyParameters =
|
||||
Object.keys(parameters).length > 0 &&
|
||||
strategyResult.result.evaluationStatus === 'incomplete'; // Use of custom strategy can be more explicit from the API
|
||||
|
||||
if (!parameters) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = [
|
||||
hasSegments && (
|
||||
<SegmentExecutionWithoutResult segments={segments} />
|
||||
),
|
||||
hasConstraints && (
|
||||
<ConstraintExecutionWithoutResults constraints={constraints} />
|
||||
),
|
||||
hasExecutionParameters && (
|
||||
<PlaygroundResultStrategyExecutionParameters
|
||||
parameters={parameters}
|
||||
constraints={constraints}
|
||||
input={input}
|
||||
/>
|
||||
),
|
||||
hasCustomStrategyParameters && (
|
||||
<CustomStrategyParams
|
||||
strategyName={name}
|
||||
parameters={parameters}
|
||||
/>
|
||||
),
|
||||
name === 'default' && (
|
||||
<StyledBoxSummary sx={{ width: '100%' }}>
|
||||
The standard strategy is <Badge color='success'>ON</Badge>{' '}
|
||||
for all users.
|
||||
</StyledBoxSummary>
|
||||
),
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
<StyledStrategyExecutionWrapper>
|
||||
{items.map((item, index) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
<Fragment key={index}>
|
||||
<ConditionallyRender
|
||||
condition={index > 0}
|
||||
show={<StrategySeparator text='AND' />}
|
||||
/>
|
||||
{item}
|
||||
</Fragment>
|
||||
))}
|
||||
</StyledStrategyExecutionWrapper>
|
||||
);
|
||||
};
|
@ -0,0 +1,47 @@
|
||||
import { Fragment, VFC } from 'react';
|
||||
import { PlaygroundSegmentSchema, PlaygroundRequestSchema } from 'openapi';
|
||||
import { ConstraintExecution } from '../ConstraintExecution/ConstraintExecution';
|
||||
import { CancelOutlined } from '@mui/icons-material';
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||
import { styled, Typography } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { SegmentItem } from 'component/common/SegmentItem/SegmentItem';
|
||||
import { ConstraintExecutionWithoutResults } from '../ConstraintExecution/ConstraintExecutionWithoutResults';
|
||||
|
||||
interface ISegmentExecutionWithoutResultProps {
|
||||
segments?: PlaygroundSegmentSchema[];
|
||||
}
|
||||
|
||||
export const SegmentExecutionWithoutResult: VFC<
|
||||
ISegmentExecutionWithoutResultProps
|
||||
> = ({ segments }) => {
|
||||
if (!segments) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{segments.map((segment, index) => (
|
||||
<Fragment key={segment.id}>
|
||||
<SegmentItem
|
||||
segment={segment}
|
||||
constraintList={
|
||||
<ConstraintExecutionWithoutResults
|
||||
constraints={segment.constraints}
|
||||
/>
|
||||
}
|
||||
isExpanded
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
// Add IF there is a next segment
|
||||
index >= 0 &&
|
||||
segments.length > 1 &&
|
||||
// Don't add if it's the last segment item
|
||||
index !== segments.length - 1
|
||||
}
|
||||
show={<StrategySeparator text='AND' />}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
@ -10,6 +10,8 @@ import { CustomStrategyParams } from './CustomStrategyParams/CustomStrategyParam
|
||||
import { formattedStrategyNames } from 'utils/strategyNames';
|
||||
import { StyledBoxSummary } from './StrategyExecution.styles';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import { ConstraintExecutionWithoutResults } from './ConstraintExecution/ConstraintExecutionWithoutResults';
|
||||
import { SegmentExecutionWithoutResult } from './SegmentExecution/SegmentExecutionWithoutResult';
|
||||
|
||||
interface IStrategyExecutionProps {
|
||||
strategyResult: PlaygroundStrategySchema;
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { FeatureStrategyItem } from './StrategyItem/FeatureStrategyItem';
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||
import { useUiFlag } from '../../../../../../../hooks/useUiFlag';
|
||||
|
||||
const StyledAlertWrapper = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
@ -31,20 +32,38 @@ const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||
interface PlaygroundResultStrategyListProps {
|
||||
strategies: PlaygroundStrategySchema[];
|
||||
input?: PlaygroundRequestSchema;
|
||||
titlePrefix?: string;
|
||||
infoText?: string;
|
||||
}
|
||||
|
||||
const StyledSubtitle = styled(Typography)(({ theme }) => ({
|
||||
margin: theme.spacing(2, 1, 2, 0),
|
||||
color: 'text.secondary',
|
||||
}));
|
||||
|
||||
export const PlaygroundResultStrategyLists = ({
|
||||
strategies,
|
||||
input,
|
||||
titlePrefix,
|
||||
infoText,
|
||||
}: PlaygroundResultStrategyListProps) => (
|
||||
<ConditionallyRender
|
||||
condition={strategies.length > 0}
|
||||
show={
|
||||
<>
|
||||
<Typography
|
||||
variant={'subtitle1'}
|
||||
sx={{ mt: 2, ml: 1, mb: 2, color: 'text.secondary' }}
|
||||
>{`Strategies (${strategies?.length})`}</Typography>
|
||||
<StyledSubtitle variant={'subtitle1'}>{`${
|
||||
titlePrefix
|
||||
? titlePrefix.concat(' strategies')
|
||||
: 'Strategies'
|
||||
} (${strategies?.length})`}</StyledSubtitle>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(infoText)}
|
||||
show={
|
||||
<StyledSubtitle variant={'subtitle2'}>
|
||||
{infoText}
|
||||
</StyledSubtitle>
|
||||
}
|
||||
/>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{strategies?.map((strategy, index) => (
|
||||
<Fragment key={strategy.id}>
|
||||
@ -91,6 +110,17 @@ export const WrappedPlaygroundResultStrategyList = ({
|
||||
feature,
|
||||
input,
|
||||
}: IWrappedPlaygroundResultStrategyListProps) => {
|
||||
const playgroundImprovements = useUiFlag('playgroundImprovements');
|
||||
const enabledStrategies = feature.strategies?.data?.filter(
|
||||
(strategy) => !strategy.disabled,
|
||||
);
|
||||
const disabledStrategies = feature.strategies?.data?.filter(
|
||||
(strategy) => strategy.disabled,
|
||||
);
|
||||
|
||||
const showDisabledStrategies =
|
||||
playgroundImprovements && disabledStrategies?.length > 0;
|
||||
|
||||
return (
|
||||
<StyledAlertWrapper sx={{ pb: 1, mt: 2 }}>
|
||||
<StyledAlert severity={'info'} color={'warning'}>
|
||||
@ -100,10 +130,26 @@ export const WrappedPlaygroundResultStrategyList = ({
|
||||
</StyledAlert>
|
||||
<StyledListWrapper sx={{ p: 2.5 }}>
|
||||
<PlaygroundResultStrategyLists
|
||||
strategies={feature.strategies?.data || []}
|
||||
strategies={enabledStrategies || []}
|
||||
input={input}
|
||||
titlePrefix={showDisabledStrategies ? 'Enabled' : ''}
|
||||
/>
|
||||
</StyledListWrapper>
|
||||
<ConditionallyRender
|
||||
condition={showDisabledStrategies}
|
||||
show={
|
||||
<StyledListWrapper sx={{ p: 2.5 }}>
|
||||
<PlaygroundResultStrategyLists
|
||||
strategies={disabledStrategies}
|
||||
input={input}
|
||||
titlePrefix={'Disabled'}
|
||||
infoText={
|
||||
'Disabled strategies are not evaluated for the overall result.'
|
||||
}
|
||||
/>
|
||||
</StyledListWrapper>
|
||||
}
|
||||
/>
|
||||
</StyledAlertWrapper>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user