From 1335da63661d476fac0c912c7076b93d38606e5b Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Thu, 19 Oct 2023 16:50:50 +0300 Subject: [PATCH] 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 --- ...aygroundResultFeatureStrategyList.test.tsx | 65 ++++++++++++++ .../PlaygroundResultFeatureStrategyList.tsx | 38 ++++++++- .../StrategyItem/FeatureStrategyItem.tsx | 31 +++++-- .../ConstraintExecutionWithoutResults.tsx | 43 ++++++++++ .../DisabledStrategyExecution.tsx | 85 +++++++++++++++++++ .../SegmentExecutionWithoutResult.tsx | 47 ++++++++++ .../StrategyExecution/StrategyExecution.tsx | 2 + .../playgroundResultStrategyLists.tsx | 56 ++++++++++-- 8 files changed, 350 insertions(+), 17 deletions(-) create mode 100644 frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintExecutionWithoutResults.tsx create mode 100644 frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/DisabledStrategyExecution.tsx create mode 100644 frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/SegmentExecution/SegmentExecutionWithoutResult.tsx diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.test.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.test.tsx index d181431a1f..3f63cbd41e 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.test.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.test.tsx @@ -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( diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.tsx index c15efe19a9..2adbcdb49d 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.tsx @@ -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 ( <> } elseShow={ - + <> + + + } + /> + } /> diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/FeatureStrategyItem.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/FeatureStrategyItem.tsx index 9c6a9f87e4..63d07dd1f2 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/FeatureStrategyItem.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/FeatureStrategyItem.tsx @@ -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 ( } > - + } + elseShow={ + + } /> ); diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintExecutionWithoutResults.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintExecutionWithoutResults.tsx new file mode 100644 index 0000000000..bcfbc342c0 --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/ConstraintExecution/ConstraintExecutionWithoutResults.tsx @@ -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 ( + + {constraints?.map((constraint, index) => ( + + 0} + show={} + /> + + + ))} + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/DisabledStrategyExecution.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/DisabledStrategyExecution.tsx new file mode 100644 index 0000000000..f195bd8f0f --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/DisabledStrategyExecution.tsx @@ -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 = + ({ 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 && ( + + ), + hasConstraints && ( + + ), + hasExecutionParameters && ( + + ), + hasCustomStrategyParameters && ( + + ), + name === 'default' && ( + + The standard strategy is ON{' '} + for all users. + + ), + ].filter(Boolean); + + return ( + + {items.map((item, index) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: + + 0} + show={} + /> + {item} + + ))} + + ); + }; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/SegmentExecution/SegmentExecutionWithoutResult.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/SegmentExecution/SegmentExecutionWithoutResult.tsx new file mode 100644 index 0000000000..33e40ac84c --- /dev/null +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/SegmentExecution/SegmentExecutionWithoutResult.tsx @@ -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) => ( + + + } + isExpanded + /> + = 0 && + segments.length > 1 && + // Don't add if it's the last segment item + index !== segments.length - 1 + } + show={} + /> + + ))} + + ); +}; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecution.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecution.tsx index 78d542ae84..7a0214f369 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecution.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/StrategyItem/StrategyExecution/StrategyExecution.tsx @@ -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; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/playgroundResultStrategyLists.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/playgroundResultStrategyLists.tsx index 308a7d8beb..aa08a59f93 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/playgroundResultStrategyLists.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/playgroundResultStrategyLists.tsx @@ -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) => ( 0} show={ <> - {`Strategies (${strategies?.length})`} + {`${ + titlePrefix + ? titlePrefix.concat(' strategies') + : 'Strategies' + } (${strategies?.length})`} + + {infoText} + + } + /> {strategies?.map((strategy, index) => ( @@ -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 ( @@ -100,10 +130,26 @@ export const WrappedPlaygroundResultStrategyList = ({ + + + + } + /> ); };