1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: disabled feature dependency (#6731)

This commit is contained in:
Mateusz Kwasniewski 2024-03-28 16:02:25 +01:00 committed by GitHub
parent 81aff26394
commit 664ceaea09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 135 additions and 19 deletions

View File

@ -42,6 +42,9 @@ export const DependencyChange: VFC<{
> >
{change.payload.feature} {change.payload.feature}
</StyledLink> </StyledLink>
{change.payload.enabled === false
? ' (disabled)'
: null}
</AddDependencyWrapper> </AddDependencyWrapper>
{actions} {actions}
</ChangeItemWrapper> </ChangeItemWrapper>

View File

@ -223,7 +223,7 @@ type ChangeRequestVariantPatch = {
type ChangeRequestEnabled = { enabled: boolean }; type ChangeRequestEnabled = { enabled: boolean };
type ChangeRequestAddDependency = { feature: string }; type ChangeRequestAddDependency = { feature: string; enabled: boolean };
export type ChangeRequestAddStrategy = Pick< export type ChangeRequestAddStrategy = Pick<
IFeatureStrategy, IFeatureStrategy,

View File

@ -11,6 +11,9 @@ const setupApi = () => {
versionInfo: { versionInfo: {
current: { oss: 'irrelevant', enterprise: 'some value' }, current: { oss: 'irrelevant', enterprise: 'some value' },
}, },
flags: {
variantDependencies: true,
},
}); });
testServerRoute( testServerRoute(
@ -103,13 +106,17 @@ test('Edit dependency', async () => {
}); });
// Open the dropdown by selecting the role. // Open the dropdown by selecting the role.
const dropdown = screen.queryAllByRole('combobox')[0]; const [featureDropdown, featureStatusDropdown] =
expect(dropdown.innerHTML).toBe('parentB'); screen.queryAllByRole('combobox');
userEvent.click(dropdown); expect(featureDropdown.innerHTML).toBe('parentB');
userEvent.click(featureDropdown);
const parentAOption = await screen.findByText('parentA'); const parentAOption = await screen.findByText('parentA');
userEvent.click(parentAOption); userEvent.click(parentAOption);
await screen.findByText('feature status');
expect(featureStatusDropdown.innerHTML).toBe('enabled');
const addButton = await screen.findByText('Add'); const addButton = await screen.findByText('Add');
userEvent.click(addButton); userEvent.click(addButton);

View File

@ -14,11 +14,13 @@ import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { DependenciesUpgradeAlert } from './DependenciesUpgradeAlert'; import { DependenciesUpgradeAlert } from './DependenciesUpgradeAlert';
import { useUiFlag } from 'hooks/useUiFlag';
interface IAddDependencyDialogueProps { interface IAddDependencyDialogueProps {
project: string; project: string;
featureId: string; featureId: string;
parentFeatureId?: string; parentFeatureId?: string;
parentFeatureValue?: ParentValue;
showDependencyDialogue: boolean; showDependencyDialogue: boolean;
onClose: () => void; onClose: () => void;
} }
@ -40,7 +42,7 @@ const LazyOptions: FC<{
parent: string; parent: string;
onSelect: (parent: string) => void; onSelect: (parent: string) => void;
}> = ({ project, featureId, parent, onSelect }) => { }> = ({ project, featureId, parent, onSelect }) => {
const { parentOptions, loading } = useParentOptions(project, featureId); const { parentOptions } = useParentOptions(project, featureId);
const options = parentOptions const options = parentOptions
? [ ? [
@ -61,10 +63,30 @@ const LazyOptions: FC<{
); );
}; };
const FeatureValueOptions: FC<{
parentValue: ParentValue;
onSelect: (parent: string) => void;
}> = ({ onSelect, parentValue }) => {
return (
<StyledSelect
fullWidth
options={[
{ key: 'enabled', label: 'enabled' },
{ key: 'disabled', label: 'disabled' },
]}
value={parentValue.status}
onChange={onSelect}
/>
);
};
type ParentValue = { status: 'enabled' } | { status: 'disabled' };
const useManageDependency = ( const useManageDependency = (
project: string, project: string,
featureId: string, featureId: string,
parent: string, parent: string,
parentValue: ParentValue,
onClose: () => void, onClose: () => void,
) => { ) => {
const { trackEvent } = usePlausibleTracker(); const { trackEvent } = usePlausibleTracker();
@ -91,7 +113,10 @@ const useManageDependency = (
{ {
action: actionType, action: actionType,
feature: featureId, feature: featureId,
payload: { feature: parent }, payload: {
feature: parent,
enabled: parentValue.status !== 'disabled',
},
}, },
]); ]);
trackEvent('dependent_features', { trackEvent('dependent_features', {
@ -105,7 +130,7 @@ const useManageDependency = (
{ action: actionType, feature: featureId, payload: undefined }, { action: actionType, feature: featureId, payload: undefined },
]); ]);
} }
refetchChangeRequests(); void refetchChangeRequests();
setToastData({ setToastData({
text: text:
actionType === 'addDependency' actionType === 'addDependency'
@ -116,7 +141,7 @@ const useManageDependency = (
}); });
}; };
const manageDependency = async () => { return async () => {
try { try {
if (isChangeRequestConfiguredInAnyEnv()) { if (isChangeRequestConfiguredInAnyEnv()) {
const actionType = const actionType =
@ -141,7 +166,10 @@ const useManageDependency = (
}); });
setToastData({ title: 'Dependency removed', type: 'success' }); setToastData({ title: 'Dependency removed', type: 'success' });
} else { } else {
await addDependency(featureId, { feature: parent }); await addDependency(featureId, {
feature: parent,
enabled: parentValue.status !== 'disabled',
});
trackEvent('dependent_features', { trackEvent('dependent_features', {
props: { props: {
eventType: 'dependency added', eventType: 'dependency added',
@ -152,32 +180,37 @@ const useManageDependency = (
} catch (error) { } catch (error) {
setToastApiError(formatUnknownError(error)); setToastApiError(formatUnknownError(error));
} }
await refetchFeature(); void refetchFeature();
onClose(); onClose();
}; };
return manageDependency;
}; };
export const AddDependencyDialogue = ({ export const AddDependencyDialogue = ({
project, project,
featureId, featureId,
parentFeatureId, parentFeatureId,
parentFeatureValue,
showDependencyDialogue, showDependencyDialogue,
onClose, onClose,
}: IAddDependencyDialogueProps) => { }: IAddDependencyDialogueProps) => {
const [parent, setParent] = useState( const [parent, setParent] = useState(
parentFeatureId || REMOVE_DEPENDENCY_OPTION.key, parentFeatureId || REMOVE_DEPENDENCY_OPTION.key,
); );
const [parentValue, setParentValue] = useState<ParentValue>(
parentFeatureValue || { status: 'enabled' },
);
const handleClick = useManageDependency( const handleClick = useManageDependency(
project, project,
featureId, featureId,
parent, parent,
parentValue,
onClose, onClose,
); );
const { isChangeRequestConfiguredInAnyEnv } = const { isChangeRequestConfiguredInAnyEnv } =
useChangeRequestsEnabled(project); useChangeRequestsEnabled(project);
const variantDependenciesEnabled = useUiFlag('variantDependencies');
return ( return (
<Dialogue <Dialogue
open={showDependencyDialogue} open={showDependencyDialogue}
@ -197,10 +230,18 @@ export const AddDependencyDialogue = ({
<DependenciesUpgradeAlert /> <DependenciesUpgradeAlert />
<Box sx={{ mt: 2, mb: 4 }}> <Box sx={{ mt: 2, mb: 4 }}>
Your feature will be evaluated only when the selected parent Your feature will be evaluated only when the selected parent
feature is enabled in the same environment. feature is{' '}
<b>
{parentValue.status === 'disabled'
? 'disabled'
: 'enabled'}
</b>{' '}
in the same environment.
</Box> </Box>
<Typography>What feature do you want to depend on?</Typography> <Typography>
What <b>feature</b> do you want to depend on?
</Typography>
<ConditionallyRender <ConditionallyRender
condition={showDependencyDialogue} condition={showDependencyDialogue}
show={ show={
@ -208,10 +249,39 @@ export const AddDependencyDialogue = ({
project={project} project={project}
featureId={featureId} featureId={featureId}
parent={parent} parent={parent}
onSelect={setParent} onSelect={(status) => {
setParentValue({ status: 'enabled' });
setParent(status);
}}
/> />
} }
/> />
<ConditionallyRender
condition={
parent !== REMOVE_DEPENDENCY_OPTION.key &&
variantDependenciesEnabled
}
show={
<Box sx={{ mt: 2 }}>
<Typography>
What <b>feature status</b> do you want to depend
on?
</Typography>
<FeatureValueOptions
parentValue={parentValue}
onSelect={(value) =>
setParentValue({
status:
value === 'disabled'
? 'disabled'
: 'enabled',
})
}
/>
</Box>
}
/>
</Box> </Box>
</Dialogue> </Dialogue>
); );

View File

@ -144,6 +144,23 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
</FlexRow> </FlexRow>
} }
/> />
<ConditionallyRender
condition={
hasParentDependency && !feature.dependencies[0]?.enabled
}
show={
<FlexRow>
<StyledDetail>
<StyledLabel>Dependency value:</StyledLabel>
<span>
{feature.dependencies[0]?.enabled
? 'enabled'
: 'disabled'}
</span>
</StyledDetail>
</FlexRow>
}
/>
<ConditionallyRender <ConditionallyRender
condition={hasChildren} condition={hasChildren}
show={ show={
@ -158,6 +175,7 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
</FlexRow> </FlexRow>
} }
/> />
<ConditionallyRender <ConditionallyRender
condition={Boolean(feature.project)} condition={Boolean(feature.project)}
show={ show={
@ -165,6 +183,12 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
project={feature.project} project={feature.project}
featureId={feature.name} featureId={feature.name}
parentFeatureId={feature.dependencies[0]?.feature} parentFeatureId={feature.dependencies[0]?.feature}
parentFeatureValue={{
status:
feature.dependencies[0]?.enabled === false
? 'disabled'
: 'enabled',
}}
onClose={() => setShowDependencyDialogue(false)} onClose={() => setShowDependencyDialogue(false)}
showDependencyDialogue={showDependencyDialogue} showDependencyDialogue={showDependencyDialogue}
/> />

View File

@ -13,6 +13,9 @@ const setupApi = () => {
versionInfo: { versionInfo: {
current: { oss: 'irrelevant', enterprise: 'some value' }, current: { oss: 'irrelevant', enterprise: 'some value' },
}, },
flags: {
variantDependencies: true,
},
}); });
testServerRoute(server, '/api/admin/projects/default/features/feature', {}); testServerRoute(server, '/api/admin/projects/default/features/feature', {});
testServerRoute( testServerRoute(
@ -250,7 +253,7 @@ test('edit dependency', async () => {
{ {
name: 'feature', name: 'feature',
project: 'default', project: 'default',
dependencies: [{ feature: 'some_parent' }], dependencies: [{ feature: 'some_parent', enabled: false }],
children: [] as string[], children: [] as string[],
} as IFeatureToggle } as IFeatureToggle
} }
@ -265,6 +268,8 @@ test('edit dependency', async () => {
await screen.findByText('Dependency:'); await screen.findByText('Dependency:');
await screen.findByText('some_parent'); await screen.findByText('some_parent');
await screen.findByText('Dependency value:');
await screen.findByText('disabled');
const actionsButton = await screen.findByRole('button', { const actionsButton = await screen.findByRole('button', {
name: /Dependency actions/i, name: /Dependency actions/i,

View File

@ -7,7 +7,6 @@ import { useLocationSettings } from 'hooks/useLocationSettings';
import { formatDateYMD } from 'utils/formatDate'; import { formatDateYMD } from 'utils/formatDate';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { FeatureEnvironmentSeen } from '../../../FeatureEnvironmentSeen/FeatureEnvironmentSeen'; import { FeatureEnvironmentSeen } from '../../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { DependencyRow } from './DependencyRow'; import { DependencyRow } from './DependencyRow';
import { FlexRow, StyledDetail, StyledLabel } from './StyledRow'; import { FlexRow, StyledDetail, StyledLabel } from './StyledRow';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
@ -30,7 +29,6 @@ export const FeatureOverviewSidePanelDetails = ({
header, header,
}: IFeatureOverviewSidePanelDetailsProps) => { }: IFeatureOverviewSidePanelDetailsProps) => {
const { locationSettings } = useLocationSettings(); const { locationSettings } = useLocationSettings();
const { uiConfig } = useUiConfig();
const showDependentFeatures = useShowDependentFeatures(feature.project); const showDependentFeatures = useShowDependentFeatures(feature.project);
const lastSeenEnvironments: ILastSeenEnvironments[] = const lastSeenEnvironments: ILastSeenEnvironments[] =

View File

@ -54,6 +54,7 @@ export interface IFeatureToggle {
export interface IDependency { export interface IDependency {
feature: string; feature: string;
enabled: boolean;
} }
export interface IFeatureEnvironment { export interface IFeatureEnvironment {

View File

@ -78,6 +78,7 @@ export type UiFlags = {
outdatedSdksBanner?: boolean; outdatedSdksBanner?: boolean;
projectOverviewRefactor?: string; projectOverviewRefactor?: string;
collectTrafficDataUsage?: boolean; collectTrafficDataUsage?: boolean;
variantDependencies?: boolean;
}; };
export interface IVersionInfo { export interface IVersionInfo {

View File

@ -143,6 +143,7 @@ exports[`should create default config 1`] = `
"stripClientHeadersOn304": false, "stripClientHeadersOn304": false,
"useMemoizedActiveTokens": false, "useMemoizedActiveTokens": false,
"userAccessUIEnabled": false, "userAccessUIEnabled": false,
"variantDependencies": false,
}, },
"externalResolver": { "externalResolver": {
"getVariant": [Function], "getVariant": [Function],

View File

@ -54,7 +54,8 @@ export type IFlagKey =
| 'displayEdgeBanner' | 'displayEdgeBanner'
| 'globalFrontendApiCache' | 'globalFrontendApiCache'
| 'returnGlobalFrontendApiCache' | 'returnGlobalFrontendApiCache'
| 'projectOverviewRefactor'; | 'projectOverviewRefactor'
| 'variantDependencies';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -267,6 +268,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_PROJECT_OVERVIEW_REFACTOR, process.env.UNLEASH_EXPERIMENTAL_PROJECT_OVERVIEW_REFACTOR,
false, false,
), ),
variantDependencies: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_VARIANT_DEPENDENCIES,
false,
),
}; };
export const defaultExperimentalOptions: IExperimentalOptions = { export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -52,6 +52,7 @@ process.nextTick(async () => {
globalFrontendApiCache: true, globalFrontendApiCache: true,
returnGlobalFrontendApiCache: false, returnGlobalFrontendApiCache: false,
projectOverviewRefactor: true, projectOverviewRefactor: true,
variantDependencies: true,
}, },
}, },
authentication: { authentication: {