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:
parent
81aff26394
commit
664ceaea09
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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,
|
||||||
|
@ -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[] =
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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],
|
||||||
|
@ -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 = {
|
||||||
|
@ -52,6 +52,7 @@ process.nextTick(async () => {
|
|||||||
globalFrontendApiCache: true,
|
globalFrontendApiCache: true,
|
||||||
returnGlobalFrontendApiCache: false,
|
returnGlobalFrontendApiCache: false,
|
||||||
projectOverviewRefactor: true,
|
projectOverviewRefactor: true,
|
||||||
|
variantDependencies: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
Loading…
Reference in New Issue
Block a user