1
0
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)
![Screenshot 2023-10-18 at 13 13
48](https://github.com/Unleash/unleash/assets/104830839/ab788241-6593-42a2-aced-412b59e308c8)

---------

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
andreas-unleash 2023-10-19 16:50:50 +03:00 committed by GitHub
parent ba53d392b2
commit 1335da6366
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 350 additions and 17 deletions

View File

@ -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(

View File

@ -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 || []}
input={input}
/>
<>
<PlaygroundResultStrategyLists
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.'
}
/>
}
/>
</>
}
/>
</>

View File

@ -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,9 +31,10 @@ export const FeatureStrategyItem = ({
return (
<StrategyItemContainer
style={{
borderColor: result.enabled
? theme.palette.success.main
: 'none',
borderColor:
result.enabled && result.evaluationStatus === 'complete'
? theme.palette.success.main
: 'none',
}}
strategy={{ ...strategy, id: `${objectId(strategy)}` }}
orderNumber={index + 1}
@ -42,10 +46,21 @@ export const FeatureStrategyItem = ({
/>
}
>
<StrategyExecution
strategyResult={strategy}
input={input}
percentageFill={theme.palette.background.elevation2}
<ConditionallyRender
condition={Boolean(strategy.disabled)}
show={
<DisabledStrategyExecution
strategyResult={strategy}
input={input}
/>
}
elseShow={
<StrategyExecution
strategyResult={strategy}
input={input}
percentageFill={theme.palette.background.elevation2}
/>
}
/>
</StrategyItemContainer>
);

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
))}
</>
);
};

View File

@ -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;

View File

@ -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>
);
};