mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-23 00:16:25 +01:00
feat: dependent features in playground (#4930)
This commit is contained in:
parent
5d11d5b0fd
commit
2c7587ba4b
@ -60,6 +60,17 @@ export const FeatureDetails = ({
|
||||
theme.palette.success.main,
|
||||
];
|
||||
|
||||
if (
|
||||
feature.hasUnsatisfiedDependency &&
|
||||
!feature.isEnabledInCurrentEnvironment
|
||||
) {
|
||||
return [
|
||||
`This feature toggle is False in ${input?.environment} because `,
|
||||
'parent dependency is not satisfied and the environment is disabled',
|
||||
theme.palette.error.main,
|
||||
];
|
||||
}
|
||||
|
||||
if (!feature.isEnabledInCurrentEnvironment)
|
||||
return [
|
||||
`This feature toggle is False in ${input?.environment} because `,
|
||||
@ -81,6 +92,14 @@ export const FeatureDetails = ({
|
||||
theme.palette.warning.main,
|
||||
];
|
||||
|
||||
if (feature.hasUnsatisfiedDependency) {
|
||||
return [
|
||||
`This feature toggle is False in ${input?.environment} because `,
|
||||
'parent dependency is not satisfied',
|
||||
theme.palette.error.main,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
`This feature toggle is False in ${input?.environment} because `,
|
||||
'all strategies are either False or could not be fully evaluated',
|
||||
|
@ -27,7 +27,10 @@ export const hasCustomStrategies = (feature: PlaygroundFeatureSchema) => {
|
||||
};
|
||||
|
||||
export const hasOnlyCustomStrategies = (feature: PlaygroundFeatureSchema) => {
|
||||
return !feature.strategies?.data?.find((strategy) =>
|
||||
DEFAULT_STRATEGIES.includes(strategy.name),
|
||||
return (
|
||||
feature.strategies?.data?.length > 0 &&
|
||||
!feature.strategies?.data?.find((strategy) =>
|
||||
DEFAULT_STRATEGIES.includes(strategy.name),
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@ -28,12 +28,13 @@ export const PlaygroundResultFeatureStrategyList = ({
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
!feature.isEnabledInCurrentEnvironment &&
|
||||
(feature.hasUnsatisfiedDependency ||
|
||||
!feature.isEnabledInCurrentEnvironment) &&
|
||||
Boolean(feature?.strategies?.data)
|
||||
}
|
||||
show={
|
||||
<WrappedPlaygroundResultStrategyList
|
||||
strategies={feature?.strategies}
|
||||
feature={feature}
|
||||
input={input}
|
||||
/>
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { Alert, Box, styled, Typography } from '@mui/material';
|
||||
import {
|
||||
PlaygroundStrategySchema,
|
||||
PlaygroundRequestSchema,
|
||||
PlaygroundFeatureSchemaStrategies,
|
||||
PlaygroundFeatureSchema,
|
||||
} from 'openapi';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { FeatureStrategyItem } from './StrategyItem/FeatureStrategyItem';
|
||||
@ -67,24 +67,40 @@ export const PlaygroundResultStrategyLists = ({
|
||||
);
|
||||
|
||||
interface IWrappedPlaygroundResultStrategyListProps {
|
||||
strategies: PlaygroundFeatureSchemaStrategies;
|
||||
feature: PlaygroundFeatureSchema;
|
||||
input?: PlaygroundRequestSchema;
|
||||
}
|
||||
|
||||
const resolveHintText = (feature: PlaygroundFeatureSchema) => {
|
||||
if (
|
||||
feature.hasUnsatisfiedDependency &&
|
||||
!feature.isEnabledInCurrentEnvironment
|
||||
) {
|
||||
return 'If environment was enabled and parent dependencies were satisfied';
|
||||
}
|
||||
if (feature.hasUnsatisfiedDependency) {
|
||||
return 'If parent dependencies were satisfied';
|
||||
}
|
||||
if (!feature.isEnabledInCurrentEnvironment) {
|
||||
return 'If environment was enabled';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const WrappedPlaygroundResultStrategyList = ({
|
||||
strategies,
|
||||
feature,
|
||||
input,
|
||||
}: IWrappedPlaygroundResultStrategyListProps) => {
|
||||
return (
|
||||
<StyledAlertWrapper sx={{ pb: 1, mt: 2 }}>
|
||||
<StyledAlert severity={'info'} color={'warning'}>
|
||||
If environment was enabled, then this feature toggle would be{' '}
|
||||
{strategies?.result ? 'TRUE' : 'FALSE'} with strategies
|
||||
{resolveHintText(feature)}, then this feature toggle would be{' '}
|
||||
{feature.strategies?.result ? 'TRUE' : 'FALSE'} with strategies
|
||||
evaluated like so:{' '}
|
||||
</StyledAlert>
|
||||
<StyledListWrapper sx={{ p: 2.5 }}>
|
||||
<PlaygroundResultStrategyLists
|
||||
strategies={strategies?.data || []}
|
||||
strategies={feature.strategies?.data || []}
|
||||
input={input}
|
||||
/>
|
||||
</StyledListWrapper>
|
||||
|
@ -19,6 +19,7 @@ export interface PlaygroundFeatureSchema {
|
||||
strategies: PlaygroundFeatureSchemaStrategies;
|
||||
/** Whether the feature is active and would be evaluated in the provided environment in a normal SDK context. */
|
||||
isEnabledInCurrentEnvironment: boolean;
|
||||
hasUnsatisfiedDependency?: boolean;
|
||||
/** Whether this feature is enabled or not in the current environment.
|
||||
If a feature can't be fully evaluated (that is, `strategies.result` is `unknown`),
|
||||
this will be `false` to align with how client SDKs treat unresolved feature states. */
|
||||
|
@ -3,5 +3,5 @@ import { FeatureDependency, FeatureDependencyId } from './dependent-features';
|
||||
export interface IDependentFeaturesStore {
|
||||
upsert(featureDependency: FeatureDependency): Promise<void>;
|
||||
delete(dependency: FeatureDependencyId): Promise<void>;
|
||||
deleteAll(child: string): Promise<void>;
|
||||
deleteAll(child?: string): Promise<void>;
|
||||
}
|
||||
|
@ -41,7 +41,13 @@ export class DependentFeaturesStore implements IDependentFeaturesStore {
|
||||
.del();
|
||||
}
|
||||
|
||||
async deleteAll(feature: string): Promise<void> {
|
||||
await this.db('dependent_features').andWhere('child', feature).del();
|
||||
async deleteAll(feature: string | undefined): Promise<void> {
|
||||
if (feature) {
|
||||
await this.db('dependent_features')
|
||||
.andWhere('child', feature)
|
||||
.del();
|
||||
} else {
|
||||
await this.db('dependent_features').del();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,9 @@ let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('advanced_playground', getLogger);
|
||||
db = await dbInit('advanced_playground', getLogger, {
|
||||
experimental: { flags: { dependentFeatures: true } },
|
||||
});
|
||||
app = await setupAppWithCustomConfig(
|
||||
db.stores,
|
||||
{
|
||||
@ -20,6 +22,7 @@ beforeAll(async () => {
|
||||
strictSchemaValidation: true,
|
||||
strategyVariant: true,
|
||||
privateProjects: true,
|
||||
dependentFeatures: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -67,6 +70,7 @@ afterAll(async () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.stores.dependentFeaturesStore.deleteAll();
|
||||
await db.stores.featureToggleStore.deleteAll();
|
||||
});
|
||||
|
||||
@ -95,6 +99,36 @@ test('advanced playground evaluation with no toggles', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('advanced playground evaluation with parent dependency', async () => {
|
||||
await createFeatureToggle('test-parent');
|
||||
await createFeatureToggle('test-child');
|
||||
await enableToggle('test-child');
|
||||
await app.addDependency('test-child', 'test-parent');
|
||||
|
||||
const { body: result } = await app.request
|
||||
.post('/api/admin/playground/advanced')
|
||||
.send({
|
||||
environments: ['default'],
|
||||
projects: ['default'],
|
||||
context: { appName: 'test' },
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200);
|
||||
|
||||
const child = result.features[0].environments.default[0];
|
||||
const parent = result.features[1].environments.default[0];
|
||||
// child is disabled because of the parent
|
||||
expect(child.hasUnsatisfiedDependency).toBe(true);
|
||||
expect(child.isEnabled).toBe(false);
|
||||
expect(child.isEnabledInCurrentEnvironment).toBe(true);
|
||||
expect(child.variant).toEqual({
|
||||
name: 'disabled',
|
||||
enabled: false,
|
||||
});
|
||||
expect(parent.hasUnsatisfiedDependency).toBe(false);
|
||||
expect(parent.isEnabled).toBe(false);
|
||||
});
|
||||
|
||||
test('advanced playground evaluation happy path', async () => {
|
||||
await createFeatureToggleWithStrategy('test-playground-feature');
|
||||
await enableToggle('test-playground-feature');
|
||||
@ -128,6 +162,7 @@ test('advanced playground evaluation happy path', async () => {
|
||||
{
|
||||
isEnabled: true,
|
||||
isEnabledInCurrentEnvironment: true,
|
||||
hasUnsatisfiedDependency: false,
|
||||
strategies: {
|
||||
result: true,
|
||||
data: [
|
||||
@ -161,6 +196,7 @@ test('advanced playground evaluation happy path', async () => {
|
||||
{
|
||||
isEnabled: true,
|
||||
isEnabledInCurrentEnvironment: true,
|
||||
hasUnsatisfiedDependency: false,
|
||||
strategies: {
|
||||
result: true,
|
||||
data: [
|
||||
@ -194,6 +230,7 @@ test('advanced playground evaluation happy path', async () => {
|
||||
{
|
||||
isEnabled: true,
|
||||
isEnabledInCurrentEnvironment: true,
|
||||
hasUnsatisfiedDependency: false,
|
||||
strategies: {
|
||||
result: true,
|
||||
data: [
|
||||
@ -227,6 +264,7 @@ test('advanced playground evaluation happy path', async () => {
|
||||
{
|
||||
isEnabled: true,
|
||||
isEnabledInCurrentEnvironment: true,
|
||||
hasUnsatisfiedDependency: false,
|
||||
strategies: {
|
||||
result: true,
|
||||
data: [
|
||||
|
@ -27,6 +27,7 @@ export type FeatureStrategiesEvaluationResult = {
|
||||
variant?: Variant;
|
||||
variants?: VariantDefinition[];
|
||||
strategies: EvaluatedPlaygroundStrategy[];
|
||||
hasUnsatisfiedDependency?: boolean;
|
||||
};
|
||||
|
||||
export default class UnleashClient {
|
||||
@ -57,13 +58,66 @@ export default class UnleashClient {
|
||||
);
|
||||
}
|
||||
|
||||
isParentDependencySatisfied(
|
||||
feature: FeatureInterface | undefined,
|
||||
context: Context,
|
||||
) {
|
||||
if (!feature?.dependencies?.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return feature.dependencies.every((parent) => {
|
||||
const parentToggle = this.repository.getToggle(parent.feature);
|
||||
|
||||
if (!parentToggle) {
|
||||
return false;
|
||||
}
|
||||
if (parentToggle.dependencies?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parent.enabled !== false) {
|
||||
if (!parentToggle.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (parent.variants?.length) {
|
||||
return parent.variants.includes(
|
||||
this.getVariant(parent.feature, context).name,
|
||||
);
|
||||
}
|
||||
return (
|
||||
this.isEnabled(parent.feature, context, () => false)
|
||||
.result === true
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
!parentToggle.enabled &&
|
||||
!(
|
||||
this.isEnabled(parent.feature, context, () => false)
|
||||
.result === true
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
isEnabled(
|
||||
name: string,
|
||||
context: Context,
|
||||
fallback: Function,
|
||||
): FeatureStrategiesEvaluationResult {
|
||||
const feature = this.repository.getToggle(name);
|
||||
return this.isFeatureEnabled(feature, context, fallback);
|
||||
|
||||
const parentDependencySatisfied = this.isParentDependencySatisfied(
|
||||
feature,
|
||||
context,
|
||||
);
|
||||
const result = this.isFeatureEnabled(feature, context, fallback);
|
||||
|
||||
return {
|
||||
...result,
|
||||
hasUnsatisfiedDependency: !parentDependencySatisfied,
|
||||
};
|
||||
}
|
||||
|
||||
isFeatureEnabled(
|
||||
@ -234,7 +288,10 @@ export default class UnleashClient {
|
||||
const fallback = fallbackVariant || getDefaultVariant();
|
||||
const feature = this.repository.getToggle(name);
|
||||
|
||||
if (typeof feature === 'undefined') {
|
||||
if (
|
||||
typeof feature === 'undefined' ||
|
||||
!this.isParentDependencySatisfied(feature, context)
|
||||
) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,12 @@ import { Segment } from './strategy/strategy';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { VariantDefinition } from './variant';
|
||||
|
||||
export interface Dependency {
|
||||
feature: string;
|
||||
variants?: string[];
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface FeatureInterface {
|
||||
name: string;
|
||||
type: string;
|
||||
@ -12,6 +18,7 @@ export interface FeatureInterface {
|
||||
impressionData: boolean;
|
||||
strategies: StrategyTransportInterface[];
|
||||
variants: VariantDefinition[];
|
||||
dependencies?: Dependency[];
|
||||
}
|
||||
|
||||
export interface ClientFeaturesResponse {
|
||||
|
@ -42,6 +42,7 @@ export const mapFeaturesForClient = (
|
||||
operator: constraint.operator as unknown as Operator,
|
||||
})),
|
||||
})),
|
||||
dependencies: feature.dependencies,
|
||||
}));
|
||||
|
||||
export const mapSegmentsForClient = (segments: ISegment[]): Segment[] =>
|
||||
|
@ -21,6 +21,7 @@ import { AdvancedPlaygroundEnvironmentFeatureSchema } from '../../openapi/spec/a
|
||||
import { validateQueryComplexity } from './validateQueryComplexity';
|
||||
import { playgroundStrategyEvaluation } from 'lib/openapi';
|
||||
import { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType';
|
||||
import { getDefaultVariant } from './feature-evaluator/variant';
|
||||
|
||||
type EvaluationInput = {
|
||||
features: FeatureConfigurationClient[];
|
||||
@ -198,23 +199,29 @@ export class PlaygroundService {
|
||||
const strategyEvaluationResult: FeatureStrategiesEvaluationResult =
|
||||
client.isEnabled(feature.name, clientContext);
|
||||
|
||||
const hasUnsatisfiedDependency =
|
||||
strategyEvaluationResult.hasUnsatisfiedDependency;
|
||||
const isEnabled =
|
||||
strategyEvaluationResult.result === true &&
|
||||
feature.enabled;
|
||||
feature.enabled &&
|
||||
!hasUnsatisfiedDependency;
|
||||
|
||||
return {
|
||||
isEnabled,
|
||||
isEnabledInCurrentEnvironment: feature.enabled,
|
||||
hasUnsatisfiedDependency,
|
||||
strategies: {
|
||||
result: strategyEvaluationResult.result,
|
||||
data: strategyEvaluationResult.strategies,
|
||||
},
|
||||
projectId: featureProject[feature.name],
|
||||
variant: client.forceGetVariant(
|
||||
feature.name,
|
||||
strategyEvaluationResult,
|
||||
clientContext,
|
||||
),
|
||||
variant: isEnabled
|
||||
? client.forceGetVariant(
|
||||
feature.name,
|
||||
strategyEvaluationResult,
|
||||
clientContext,
|
||||
)
|
||||
: getDefaultVariant(),
|
||||
name: feature.name,
|
||||
environment,
|
||||
context,
|
||||
|
@ -67,6 +67,11 @@ export const playgroundFeatureSchema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
hasUnsatisfiedDependency: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether the feature has a parent dependency that is not satisfied',
|
||||
},
|
||||
isEnabledInCurrentEnvironment: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
|
Loading…
Reference in New Issue
Block a user