diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/DependencyChange.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/DependencyChange.tsx index 390673ee2f1..a71e3e68999 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/DependencyChange.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/DependencyChange.tsx @@ -42,6 +42,9 @@ export const DependencyChange: VFC<{ > {change.payload.feature} + {change.payload.enabled === false + ? ' (disabled)' + : null} {actions} diff --git a/frontend/src/component/changeRequest/changeRequest.types.ts b/frontend/src/component/changeRequest/changeRequest.types.ts index 5ab0602efaa..1de55729380 100644 --- a/frontend/src/component/changeRequest/changeRequest.types.ts +++ b/frontend/src/component/changeRequest/changeRequest.types.ts @@ -223,7 +223,7 @@ type ChangeRequestVariantPatch = { type ChangeRequestEnabled = { enabled: boolean }; -type ChangeRequestAddDependency = { feature: string }; +type ChangeRequestAddDependency = { feature: string; enabled: boolean }; export type ChangeRequestAddStrategy = Pick< IFeatureStrategy, diff --git a/frontend/src/component/feature/Dependencies/AddDependencyDialogue.test.tsx b/frontend/src/component/feature/Dependencies/AddDependencyDialogue.test.tsx index 8aa4eed12bb..e35106a6fe1 100644 --- a/frontend/src/component/feature/Dependencies/AddDependencyDialogue.test.tsx +++ b/frontend/src/component/feature/Dependencies/AddDependencyDialogue.test.tsx @@ -11,6 +11,9 @@ const setupApi = () => { versionInfo: { current: { oss: 'irrelevant', enterprise: 'some value' }, }, + flags: { + variantDependencies: true, + }, }); testServerRoute( @@ -103,13 +106,17 @@ test('Edit dependency', async () => { }); // Open the dropdown by selecting the role. - const dropdown = screen.queryAllByRole('combobox')[0]; - expect(dropdown.innerHTML).toBe('parentB'); - userEvent.click(dropdown); + const [featureDropdown, featureStatusDropdown] = + screen.queryAllByRole('combobox'); + expect(featureDropdown.innerHTML).toBe('parentB'); + userEvent.click(featureDropdown); const parentAOption = await screen.findByText('parentA'); userEvent.click(parentAOption); + await screen.findByText('feature status'); + expect(featureStatusDropdown.innerHTML).toBe('enabled'); + const addButton = await screen.findByText('Add'); userEvent.click(addButton); diff --git a/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx b/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx index 35a0111db27..bd418453cf3 100644 --- a/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx +++ b/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx @@ -14,11 +14,13 @@ import useToast from 'hooks/useToast'; import { formatUnknownError } from 'utils/formatUnknownError'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { DependenciesUpgradeAlert } from './DependenciesUpgradeAlert'; +import { useUiFlag } from 'hooks/useUiFlag'; interface IAddDependencyDialogueProps { project: string; featureId: string; parentFeatureId?: string; + parentFeatureValue?: ParentValue; showDependencyDialogue: boolean; onClose: () => void; } @@ -40,7 +42,7 @@ const LazyOptions: FC<{ parent: string; onSelect: (parent: string) => void; }> = ({ project, featureId, parent, onSelect }) => { - const { parentOptions, loading } = useParentOptions(project, featureId); + const { parentOptions } = useParentOptions(project, featureId); const options = parentOptions ? [ @@ -61,10 +63,30 @@ const LazyOptions: FC<{ ); }; +const FeatureValueOptions: FC<{ + parentValue: ParentValue; + onSelect: (parent: string) => void; +}> = ({ onSelect, parentValue }) => { + return ( + + ); +}; + +type ParentValue = { status: 'enabled' } | { status: 'disabled' }; + const useManageDependency = ( project: string, featureId: string, parent: string, + parentValue: ParentValue, onClose: () => void, ) => { const { trackEvent } = usePlausibleTracker(); @@ -91,7 +113,10 @@ const useManageDependency = ( { action: actionType, feature: featureId, - payload: { feature: parent }, + payload: { + feature: parent, + enabled: parentValue.status !== 'disabled', + }, }, ]); trackEvent('dependent_features', { @@ -105,7 +130,7 @@ const useManageDependency = ( { action: actionType, feature: featureId, payload: undefined }, ]); } - refetchChangeRequests(); + void refetchChangeRequests(); setToastData({ text: actionType === 'addDependency' @@ -116,7 +141,7 @@ const useManageDependency = ( }); }; - const manageDependency = async () => { + return async () => { try { if (isChangeRequestConfiguredInAnyEnv()) { const actionType = @@ -141,7 +166,10 @@ const useManageDependency = ( }); setToastData({ title: 'Dependency removed', type: 'success' }); } else { - await addDependency(featureId, { feature: parent }); + await addDependency(featureId, { + feature: parent, + enabled: parentValue.status !== 'disabled', + }); trackEvent('dependent_features', { props: { eventType: 'dependency added', @@ -152,32 +180,37 @@ const useManageDependency = ( } catch (error) { setToastApiError(formatUnknownError(error)); } - await refetchFeature(); + void refetchFeature(); onClose(); }; - - return manageDependency; }; export const AddDependencyDialogue = ({ project, featureId, parentFeatureId, + parentFeatureValue, showDependencyDialogue, onClose, }: IAddDependencyDialogueProps) => { const [parent, setParent] = useState( parentFeatureId || REMOVE_DEPENDENCY_OPTION.key, ); + const [parentValue, setParentValue] = useState( + parentFeatureValue || { status: 'enabled' }, + ); const handleClick = useManageDependency( project, featureId, parent, + parentValue, onClose, ); const { isChangeRequestConfiguredInAnyEnv } = useChangeRequestsEnabled(project); + const variantDependenciesEnabled = useUiFlag('variantDependencies'); + return ( Your feature will be evaluated only when the selected parent - feature is enabled in the same environment. + feature is{' '} + + {parentValue.status === 'disabled' + ? 'disabled' + : 'enabled'} + {' '} + in the same environment. - What feature do you want to depend on? + + What feature do you want to depend on? + { + setParentValue({ status: 'enabled' }); + setParent(status); + }} /> } /> + + + + What feature status do you want to depend + on? + + + setParentValue({ + status: + value === 'disabled' + ? 'disabled' + : 'enabled', + }) + } + /> + + } + /> ); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/DependencyRow.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/DependencyRow.tsx index c2269af0fa3..615931e1971 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/DependencyRow.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/DependencyRow.tsx @@ -144,6 +144,23 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => { } /> + + + Dependency value: + + {feature.dependencies[0]?.enabled + ? 'enabled' + : 'disabled'} + + + + } + /> = ({ feature }) => { } /> + = ({ feature }) => { project={feature.project} featureId={feature.name} parentFeatureId={feature.dependencies[0]?.feature} + parentFeatureValue={{ + status: + feature.dependencies[0]?.enabled === false + ? 'disabled' + : 'enabled', + }} onClose={() => setShowDependencyDialogue(false)} showDependencyDialogue={showDependencyDialogue} /> diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.test.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.test.tsx index b9628e6f878..b36d5d06cd3 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.test.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.test.tsx @@ -13,6 +13,9 @@ const setupApi = () => { versionInfo: { current: { oss: 'irrelevant', enterprise: 'some value' }, }, + flags: { + variantDependencies: true, + }, }); testServerRoute(server, '/api/admin/projects/default/features/feature', {}); testServerRoute( @@ -250,7 +253,7 @@ test('edit dependency', async () => { { name: 'feature', project: 'default', - dependencies: [{ feature: 'some_parent' }], + dependencies: [{ feature: 'some_parent', enabled: false }], children: [] as string[], } as IFeatureToggle } @@ -265,6 +268,8 @@ test('edit dependency', async () => { await screen.findByText('Dependency:'); await screen.findByText('some_parent'); + await screen.findByText('Dependency value:'); + await screen.findByText('disabled'); const actionsButton = await screen.findByRole('button', { name: /Dependency actions/i, diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.tsx index c3641f76994..f1a247d80b7 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails.tsx @@ -7,7 +7,6 @@ import { useLocationSettings } from 'hooks/useLocationSettings'; import { formatDateYMD } from 'utils/formatDate'; import { parseISO } from 'date-fns'; import { FeatureEnvironmentSeen } from '../../../FeatureEnvironmentSeen/FeatureEnvironmentSeen'; -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { DependencyRow } from './DependencyRow'; import { FlexRow, StyledDetail, StyledLabel } from './StyledRow'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; @@ -30,7 +29,6 @@ export const FeatureOverviewSidePanelDetails = ({ header, }: IFeatureOverviewSidePanelDetailsProps) => { const { locationSettings } = useLocationSettings(); - const { uiConfig } = useUiConfig(); const showDependentFeatures = useShowDependentFeatures(feature.project); const lastSeenEnvironments: ILastSeenEnvironments[] = diff --git a/frontend/src/interfaces/featureToggle.ts b/frontend/src/interfaces/featureToggle.ts index 10f35b0db14..0f3628d71dc 100644 --- a/frontend/src/interfaces/featureToggle.ts +++ b/frontend/src/interfaces/featureToggle.ts @@ -54,6 +54,7 @@ export interface IFeatureToggle { export interface IDependency { feature: string; + enabled: boolean; } export interface IFeatureEnvironment { diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index 9f822d7067f..dd6bb98e823 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -78,6 +78,7 @@ export type UiFlags = { outdatedSdksBanner?: boolean; projectOverviewRefactor?: string; collectTrafficDataUsage?: boolean; + variantDependencies?: boolean; }; export interface IVersionInfo { diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 3fef41519b3..4e0c02fd688 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -143,6 +143,7 @@ exports[`should create default config 1`] = ` "stripClientHeadersOn304": false, "useMemoizedActiveTokens": false, "userAccessUIEnabled": false, + "variantDependencies": false, }, "externalResolver": { "getVariant": [Function], diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 21b9eff133d..7a73c1024bc 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -54,7 +54,8 @@ export type IFlagKey = | 'displayEdgeBanner' | 'globalFrontendApiCache' | 'returnGlobalFrontendApiCache' - | 'projectOverviewRefactor'; + | 'projectOverviewRefactor' + | 'variantDependencies'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -267,6 +268,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_PROJECT_OVERVIEW_REFACTOR, false, ), + variantDependencies: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_VARIANT_DEPENDENCIES, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = { diff --git a/src/server-dev.ts b/src/server-dev.ts index 10a4d6dee63..669f01f4a28 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -52,6 +52,7 @@ process.nextTick(async () => { globalFrontendApiCache: true, returnGlobalFrontendApiCache: false, projectOverviewRefactor: true, + variantDependencies: true, }, }, authentication: {