mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: message banner (variants) (#3788)
-
https://linear.app/unleash/issue/2-546/fetch-the-payload-from-a-real-feature-flag
-
https://linear.app/unleash/issue/2-547/adapt-ui-to-use-the-feature-flag-information-were-fetching
Tackles the 2 tasks above. 
Adapts our `FlagResolver` logic to support variants, so we can use them
for our message banner project but also anything else in the future.
Also adapts MessageBanner to the new logic.
 - Add support for variants in `FlagResolver`;
 - Adapt `MessageBanner` to a variants flag;
 - Adds `sticky` support for the `MessageBanner`;
- Adds our first variants flag to `uiConfig` and `experimental`:
`messageBanner`;
- Adds a `variant-flag-schema` to make it easy to represent the variant
output that we specify in `uiConfig`;
- Adapts `experimental` to be able to represent default variants while
still maintaining type safety;
- Adds helpers to make it easy to use variants in our project, such as
`getVariantValue` and the `useVariant` hook;
 - Adapts and adds new tests in `flag-resolver.test.ts`;
 
### Notes
- ~~The `as PayloadType` assertions need
https://github.com/Unleash/unleash-client-node/pull/454 since it
includes https://github.com/Unleash/unleash-client-node/pull/452~~
(50ccf60893);
 - ~~Enterprise needs a PR that will follow soon~~;
 
 

			
			
This commit is contained in:
		
							parent
							
								
									2487b990bd
								
							
						
					
					
						commit
						db61a8a40c
					
				@ -6,11 +6,13 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import { MessageBannerDialog } from './MessageBannerDialog/MessageBannerDialog';
 | 
			
		||||
import { useState } from 'react';
 | 
			
		||||
import ReactMarkdown from 'react-markdown';
 | 
			
		||||
import { useVariant } from 'hooks/useVariant';
 | 
			
		||||
 | 
			
		||||
const StyledBar = styled('aside', {
 | 
			
		||||
    shouldForwardProp: prop => prop !== 'variant',
 | 
			
		||||
})<{ variant: BannerVariant }>(({ theme, variant }) => ({
 | 
			
		||||
    position: 'relative',
 | 
			
		||||
    shouldForwardProp: prop => prop !== 'variant' && prop !== 'sticky',
 | 
			
		||||
})<{ variant: BannerVariant; sticky?: boolean }>(
 | 
			
		||||
    ({ theme, variant, sticky }) => ({
 | 
			
		||||
        position: sticky ? 'sticky' : 'relative',
 | 
			
		||||
        zIndex: 1,
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        alignItems: 'center',
 | 
			
		||||
@ -22,7 +24,12 @@ const StyledBar = styled('aside', {
 | 
			
		||||
        background: theme.palette[variant].light,
 | 
			
		||||
        color: theme.palette[variant].dark,
 | 
			
		||||
        fontSize: theme.fontSizes.smallBody,
 | 
			
		||||
}));
 | 
			
		||||
        ...(sticky && {
 | 
			
		||||
            top: 0,
 | 
			
		||||
            zIndex: theme.zIndex.sticky,
 | 
			
		||||
        }),
 | 
			
		||||
    })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const StyledIcon = styled('div', {
 | 
			
		||||
    shouldForwardProp: prop => prop !== 'variant',
 | 
			
		||||
@ -44,6 +51,7 @@ interface IMessageFlag {
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
    message: string;
 | 
			
		||||
    variant?: BannerVariant;
 | 
			
		||||
    sticky?: boolean;
 | 
			
		||||
    icon?: string;
 | 
			
		||||
    link?: string;
 | 
			
		||||
    linkText?: string;
 | 
			
		||||
@ -52,61 +60,30 @@ interface IMessageFlag {
 | 
			
		||||
    dialog?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: Grab a real feature flag instead
 | 
			
		||||
/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
			
		||||
const mockFlag: IMessageFlag = {
 | 
			
		||||
    enabled: true,
 | 
			
		||||
    message:
 | 
			
		||||
        '**Heads up!** It seems like one of your client instances might be misbehaving.',
 | 
			
		||||
    variant: 'warning',
 | 
			
		||||
    link: '/admin/network',
 | 
			
		||||
    linkText: 'View Network',
 | 
			
		||||
    plausibleEvent: 'network_warning',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mockFlag2: IMessageFlag = {
 | 
			
		||||
    enabled: true,
 | 
			
		||||
    message:
 | 
			
		||||
        '**Unleash v5 is finally here!** Check out what changed in the newest major release.',
 | 
			
		||||
    variant: 'secondary',
 | 
			
		||||
    link: 'dialog',
 | 
			
		||||
    linkText: "What's new?",
 | 
			
		||||
    plausibleEvent: 'change_log_v5',
 | 
			
		||||
    dialog: `
 | 
			
		||||
## Unleash v5 🎉
 | 
			
		||||
**Unleash v5 is finally here!**
 | 
			
		||||
 | 
			
		||||
Check out what changed in the newest major release:
 | 
			
		||||
 | 
			
		||||
- An Amazing Feature
 | 
			
		||||
- Another Amazing Feature
 | 
			
		||||
- We'll save the best for last
 | 
			
		||||
- And the best is...
 | 
			
		||||
- **Unleash v5 is finally here!**
 | 
			
		||||
 | 
			
		||||
You can read more about it on our newest [blog post](https://www.getunleash.io/blog).`,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const MessageBanner = () => {
 | 
			
		||||
    const { uiConfig } = useUiConfig();
 | 
			
		||||
    const [open, setOpen] = useState(false);
 | 
			
		||||
 | 
			
		||||
    const messageBanner = useVariant<IMessageFlag>(
 | 
			
		||||
        uiConfig.flags.messageBanner
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!messageBanner) return null;
 | 
			
		||||
 | 
			
		||||
    const {
 | 
			
		||||
        enabled,
 | 
			
		||||
        message,
 | 
			
		||||
        variant = 'neutral',
 | 
			
		||||
        sticky,
 | 
			
		||||
        icon,
 | 
			
		||||
        link,
 | 
			
		||||
        linkText = 'More info',
 | 
			
		||||
        plausibleEvent,
 | 
			
		||||
        dialogTitle,
 | 
			
		||||
        dialog,
 | 
			
		||||
    } = { ...mockFlag2, enabled: uiConfig.flags.messageBanner };
 | 
			
		||||
 | 
			
		||||
    if (!enabled) return null;
 | 
			
		||||
    } = messageBanner;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledBar variant={variant}>
 | 
			
		||||
        <StyledBar variant={variant} sticky={sticky}>
 | 
			
		||||
            <StyledIcon variant={variant}>
 | 
			
		||||
                <BannerIcon icon={icon} variant={variant} />
 | 
			
		||||
            </StyledIcon>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								frontend/src/hooks/useVariant.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/hooks/useVariant.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
import { useMemo } from 'react';
 | 
			
		||||
import { Variant, getVariantValue } from 'utils/variants';
 | 
			
		||||
 | 
			
		||||
export const useVariant = <T = string>(variant?: Variant) => {
 | 
			
		||||
    return useMemo(() => {
 | 
			
		||||
        if (variant?.enabled) {
 | 
			
		||||
            return getVariantValue<T>(variant);
 | 
			
		||||
        }
 | 
			
		||||
    }, [variant]);
 | 
			
		||||
};
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import { ReactNode } from 'react';
 | 
			
		||||
import { Variant } from 'utils/variants';
 | 
			
		||||
 | 
			
		||||
export interface IUiConfig {
 | 
			
		||||
    authenticationType?: string;
 | 
			
		||||
@ -38,7 +39,7 @@ export interface IFlags {
 | 
			
		||||
    UG?: boolean;
 | 
			
		||||
    embedProxyFrontend?: boolean;
 | 
			
		||||
    maintenanceMode?: boolean;
 | 
			
		||||
    messageBanner?: boolean;
 | 
			
		||||
    messageBanner?: Variant;
 | 
			
		||||
    featuresExportImport?: boolean;
 | 
			
		||||
    caseInsensitiveInOperators?: boolean;
 | 
			
		||||
    proPlanAutoCharge?: boolean;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								frontend/src/utils/variants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								frontend/src/utils/variants.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
export enum PayloadType {
 | 
			
		||||
    STRING = 'string',
 | 
			
		||||
    JSON = 'json',
 | 
			
		||||
    CSV = 'csv',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Payload {
 | 
			
		||||
    type: PayloadType;
 | 
			
		||||
    value: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Variant {
 | 
			
		||||
    name: string;
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
    payload?: Payload;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getVariantValue = <T = string>(
 | 
			
		||||
    variant: Variant | undefined
 | 
			
		||||
): T | undefined => {
 | 
			
		||||
    if (variant?.payload !== undefined) {
 | 
			
		||||
        if (variant.payload.type === PayloadType.JSON) {
 | 
			
		||||
            return JSON.parse(variant.payload.value) as T;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return variant.payload.value as T;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
@ -146,7 +146,7 @@
 | 
			
		||||
    "stoppable": "^1.1.0",
 | 
			
		||||
    "ts-toolbelt": "^9.6.0",
 | 
			
		||||
    "type-is": "^1.6.18",
 | 
			
		||||
    "unleash-client": "3.18.1",
 | 
			
		||||
    "unleash-client": "3.20.0",
 | 
			
		||||
    "uuid": "^9.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
 | 
			
		||||
@ -63,6 +63,7 @@ exports[`should create default config 1`] = `
 | 
			
		||||
  },
 | 
			
		||||
  "experimental": {
 | 
			
		||||
    "externalResolver": {
 | 
			
		||||
      "getVariant": [Function],
 | 
			
		||||
      "isEnabled": [Function],
 | 
			
		||||
    },
 | 
			
		||||
    "flags": {
 | 
			
		||||
@ -76,7 +77,14 @@ exports[`should create default config 1`] = `
 | 
			
		||||
      "googleAuthEnabled": false,
 | 
			
		||||
      "groupRootRoles": false,
 | 
			
		||||
      "maintenanceMode": false,
 | 
			
		||||
      "messageBanner": false,
 | 
			
		||||
      "messageBanner": {
 | 
			
		||||
        "enabled": false,
 | 
			
		||||
        "name": "message-banner",
 | 
			
		||||
        "payload": {
 | 
			
		||||
          "type": "json",
 | 
			
		||||
          "value": "",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      "migrationLock": false,
 | 
			
		||||
      "personalAccessTokensKillSwitch": false,
 | 
			
		||||
      "proPlanAutoCharge": false,
 | 
			
		||||
@ -98,7 +106,14 @@ exports[`should create default config 1`] = `
 | 
			
		||||
      "googleAuthEnabled": false,
 | 
			
		||||
      "groupRootRoles": false,
 | 
			
		||||
      "maintenanceMode": false,
 | 
			
		||||
      "messageBanner": false,
 | 
			
		||||
      "messageBanner": {
 | 
			
		||||
        "enabled": false,
 | 
			
		||||
        "name": "message-banner",
 | 
			
		||||
        "payload": {
 | 
			
		||||
          "type": "json",
 | 
			
		||||
          "value": "",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      "migrationLock": false,
 | 
			
		||||
      "personalAccessTokensKillSwitch": false,
 | 
			
		||||
      "proPlanAutoCharge": false,
 | 
			
		||||
@ -108,6 +123,7 @@ exports[`should create default config 1`] = `
 | 
			
		||||
      "variantMetrics": false,
 | 
			
		||||
    },
 | 
			
		||||
    "externalResolver": {
 | 
			
		||||
      "getVariant": [Function],
 | 
			
		||||
      "isEnabled": [Function],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -134,6 +134,7 @@ import {
 | 
			
		||||
    validatePasswordSchema,
 | 
			
		||||
    validateTagTypeSchema,
 | 
			
		||||
    variantSchema,
 | 
			
		||||
    variantFlagSchema,
 | 
			
		||||
    variantsSchema,
 | 
			
		||||
    versionSchema,
 | 
			
		||||
} from './spec';
 | 
			
		||||
@ -319,6 +320,7 @@ export const schemas: UnleashSchemas = {
 | 
			
		||||
    validatePasswordSchema,
 | 
			
		||||
    validateTagTypeSchema,
 | 
			
		||||
    variantSchema,
 | 
			
		||||
    variantFlagSchema,
 | 
			
		||||
    variantsSchema,
 | 
			
		||||
    versionSchema,
 | 
			
		||||
    projectOverviewSchema,
 | 
			
		||||
 | 
			
		||||
@ -168,6 +168,7 @@ const metaRules: Rule[] = [
 | 
			
		||||
            'validatePasswordSchema',
 | 
			
		||||
            'validateTagTypeSchema',
 | 
			
		||||
            'variantSchema',
 | 
			
		||||
            'variantFlagSchema',
 | 
			
		||||
            'versionSchema',
 | 
			
		||||
            'projectOverviewSchema',
 | 
			
		||||
            'importTogglesSchema',
 | 
			
		||||
@ -277,6 +278,7 @@ const metaRules: Rule[] = [
 | 
			
		||||
            'validatePasswordSchema',
 | 
			
		||||
            'validateTagTypeSchema',
 | 
			
		||||
            'variantSchema',
 | 
			
		||||
            'variantFlagSchema',
 | 
			
		||||
            'variantsSchema',
 | 
			
		||||
            'versionSchema',
 | 
			
		||||
            'importTogglesSchema',
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ export * from './profile-schema';
 | 
			
		||||
export * from './project-schema';
 | 
			
		||||
export * from './segment-schema';
 | 
			
		||||
export * from './variant-schema';
 | 
			
		||||
export * from './variant-flag-schema';
 | 
			
		||||
export * from './version-schema';
 | 
			
		||||
export * from './features-schema';
 | 
			
		||||
export * from './feedback-schema';
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import { FromSchema } from 'json-schema-to-ts';
 | 
			
		||||
import { PayloadType } from 'unleash-client';
 | 
			
		||||
 | 
			
		||||
export const proxyFeatureSchema = {
 | 
			
		||||
    $id: '#/components/schemas/proxyFeatureSchema',
 | 
			
		||||
@ -31,7 +32,10 @@ export const proxyFeatureSchema = {
 | 
			
		||||
                    additionalProperties: false,
 | 
			
		||||
                    required: ['type', 'value'],
 | 
			
		||||
                    properties: {
 | 
			
		||||
                        type: { type: 'string', enum: ['string'] },
 | 
			
		||||
                        type: {
 | 
			
		||||
                            type: 'string',
 | 
			
		||||
                            enum: Object.values(PayloadType),
 | 
			
		||||
                        },
 | 
			
		||||
                        value: { type: 'string' },
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import { FromSchema } from 'json-schema-to-ts';
 | 
			
		||||
import { versionSchema } from './version-schema';
 | 
			
		||||
import { variantFlagSchema } from './variant-flag-schema';
 | 
			
		||||
 | 
			
		||||
export const uiConfigSchema = {
 | 
			
		||||
    $id: '#/components/schemas/uiConfigSchema',
 | 
			
		||||
@ -52,8 +53,15 @@ export const uiConfigSchema = {
 | 
			
		||||
        flags: {
 | 
			
		||||
            type: 'object',
 | 
			
		||||
            additionalProperties: {
 | 
			
		||||
                anyOf: [
 | 
			
		||||
                    {
 | 
			
		||||
                        type: 'boolean',
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        $ref: '#/components/schemas/variantFlagSchema',
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        links: {
 | 
			
		||||
            type: 'array',
 | 
			
		||||
@ -79,6 +87,7 @@ export const uiConfigSchema = {
 | 
			
		||||
    components: {
 | 
			
		||||
        schemas: {
 | 
			
		||||
            versionSchema,
 | 
			
		||||
            variantFlagSchema,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										30
									
								
								src/lib/openapi/spec/variant-flag-schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/lib/openapi/spec/variant-flag-schema.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
import { FromSchema } from 'json-schema-to-ts';
 | 
			
		||||
 | 
			
		||||
export const variantFlagSchema = {
 | 
			
		||||
    $id: '#/components/schemas/variantFlagSchema',
 | 
			
		||||
    type: 'object',
 | 
			
		||||
    additionalProperties: false,
 | 
			
		||||
    properties: {
 | 
			
		||||
        name: {
 | 
			
		||||
            type: 'string',
 | 
			
		||||
        },
 | 
			
		||||
        enabled: {
 | 
			
		||||
            type: 'boolean',
 | 
			
		||||
        },
 | 
			
		||||
        payload: {
 | 
			
		||||
            type: 'object',
 | 
			
		||||
            additionalProperties: false,
 | 
			
		||||
            properties: {
 | 
			
		||||
                type: {
 | 
			
		||||
                    type: 'string',
 | 
			
		||||
                },
 | 
			
		||||
                value: {
 | 
			
		||||
                    type: 'string',
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    components: {},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export type VariantFlagSchema = FromSchema<typeof variantFlagSchema>;
 | 
			
		||||
@ -1,9 +1,29 @@
 | 
			
		||||
import { Variant, PayloadType } from 'unleash-client';
 | 
			
		||||
import { parseEnvVarBoolean } from '../util';
 | 
			
		||||
 | 
			
		||||
export type IFlags = Partial<typeof flags>;
 | 
			
		||||
export type IFlagKey = keyof IFlags;
 | 
			
		||||
export type IFlagKey =
 | 
			
		||||
    | 'anonymiseEventLog'
 | 
			
		||||
    | 'embedProxy'
 | 
			
		||||
    | 'embedProxyFrontend'
 | 
			
		||||
    | 'responseTimeWithAppNameKillSwitch'
 | 
			
		||||
    | 'maintenanceMode'
 | 
			
		||||
    | 'messageBanner'
 | 
			
		||||
    | 'featuresExportImport'
 | 
			
		||||
    | 'caseInsensitiveInOperators'
 | 
			
		||||
    | 'strictSchemaValidation'
 | 
			
		||||
    | 'proPlanAutoCharge'
 | 
			
		||||
    | 'personalAccessTokensKillSwitch'
 | 
			
		||||
    | 'cleanClientApi'
 | 
			
		||||
    | 'groupRootRoles'
 | 
			
		||||
    | 'migrationLock'
 | 
			
		||||
    | 'demo'
 | 
			
		||||
    | 'strategyImprovements'
 | 
			
		||||
    | 'googleAuthEnabled'
 | 
			
		||||
    | 'variantMetrics';
 | 
			
		||||
 | 
			
		||||
const flags = {
 | 
			
		||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
 | 
			
		||||
 | 
			
		||||
const flags: IFlags = {
 | 
			
		||||
    anonymiseEventLog: false,
 | 
			
		||||
    embedProxy: parseEnvVarBoolean(
 | 
			
		||||
        process.env.UNLEASH_EXPERIMENTAL_EMBED_PROXY,
 | 
			
		||||
@ -21,10 +41,18 @@ const flags = {
 | 
			
		||||
        process.env.UNLEASH_EXPERIMENTAL_MAINTENANCE_MODE,
 | 
			
		||||
        false,
 | 
			
		||||
    ),
 | 
			
		||||
    messageBanner: parseEnvVarBoolean(
 | 
			
		||||
    messageBanner: {
 | 
			
		||||
        name: 'message-banner',
 | 
			
		||||
        enabled: parseEnvVarBoolean(
 | 
			
		||||
            process.env.UNLEASH_EXPERIMENTAL_MESSAGE_BANNER,
 | 
			
		||||
            false,
 | 
			
		||||
        ),
 | 
			
		||||
        payload: {
 | 
			
		||||
            type: PayloadType.JSON,
 | 
			
		||||
            value:
 | 
			
		||||
                process.env.UNLEASH_EXPERIMENTAL_MESSAGE_BANNER_PAYLOAD ?? '',
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    featuresExportImport: parseEnvVarBoolean(
 | 
			
		||||
        process.env.UNLEASH_EXPERIMENTAL_FEATURES_EXPORT_IMPORT,
 | 
			
		||||
        true,
 | 
			
		||||
@ -68,7 +96,10 @@ const flags = {
 | 
			
		||||
 | 
			
		||||
export const defaultExperimentalOptions: IExperimentalOptions = {
 | 
			
		||||
    flags,
 | 
			
		||||
    externalResolver: { isEnabled: (): boolean => false },
 | 
			
		||||
    externalResolver: {
 | 
			
		||||
        isEnabled: (): boolean => false,
 | 
			
		||||
        getVariant: () => undefined,
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface IExperimentalOptions {
 | 
			
		||||
@ -83,8 +114,16 @@ export interface IFlagContext {
 | 
			
		||||
export interface IFlagResolver {
 | 
			
		||||
    getAll: (context?: IFlagContext) => IFlags;
 | 
			
		||||
    isEnabled: (expName: IFlagKey, context?: IFlagContext) => boolean;
 | 
			
		||||
    getVariant: (
 | 
			
		||||
        expName: IFlagKey,
 | 
			
		||||
        context?: IFlagContext,
 | 
			
		||||
    ) => Variant | undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IExternalFlagResolver {
 | 
			
		||||
    isEnabled: (flagName: IFlagKey, context?: IFlagContext) => boolean;
 | 
			
		||||
    getVariant: (
 | 
			
		||||
        flagName: IFlagKey,
 | 
			
		||||
        context?: IFlagContext,
 | 
			
		||||
    ) => Variant | undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,10 +3,7 @@ import { Context } from './context';
 | 
			
		||||
import { FeatureInterface } from './feature';
 | 
			
		||||
import normalizedValue from './strategy/util';
 | 
			
		||||
import { resolveContextValue } from './helpers';
 | 
			
		||||
 | 
			
		||||
enum PayloadType {
 | 
			
		||||
    STRING = 'string',
 | 
			
		||||
}
 | 
			
		||||
import { PayloadType } from 'unleash-client';
 | 
			
		||||
 | 
			
		||||
interface Override {
 | 
			
		||||
    contextName: string;
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import { PayloadType } from 'unleash-client';
 | 
			
		||||
import { defaultExperimentalOptions, IFlagKey } from '../types/experimental';
 | 
			
		||||
import FlagResolver from './flag-resolver';
 | 
			
		||||
import FlagResolver, { getVariantValue } from './flag-resolver';
 | 
			
		||||
import { IExperimentalOptions } from '../types/experimental';
 | 
			
		||||
 | 
			
		||||
test('should produce empty exposed flags', () => {
 | 
			
		||||
@ -29,6 +30,7 @@ test('should use external resolver for dynamic flags', () => {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        getVariant: () => undefined,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const config = {
 | 
			
		||||
@ -48,6 +50,7 @@ test('should not use external resolver for enabled experiments', () => {
 | 
			
		||||
        isEnabled: () => {
 | 
			
		||||
            return false;
 | 
			
		||||
        },
 | 
			
		||||
        getVariant: () => undefined,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const config = {
 | 
			
		||||
@ -67,6 +70,7 @@ test('should load experimental flags', () => {
 | 
			
		||||
        isEnabled: () => {
 | 
			
		||||
            return false;
 | 
			
		||||
        },
 | 
			
		||||
        getVariant: () => undefined,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const config = {
 | 
			
		||||
@ -87,6 +91,7 @@ test('should load experimental flags from external provider', () => {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        getVariant: () => undefined,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const config = {
 | 
			
		||||
@ -99,3 +104,60 @@ test('should load experimental flags from external provider', () => {
 | 
			
		||||
    expect(resolver.isEnabled('someFlag' as IFlagKey)).toBe(true);
 | 
			
		||||
    expect(resolver.isEnabled('extraFlag' as IFlagKey)).toBe(true);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should support variant flags', () => {
 | 
			
		||||
    const variant = {
 | 
			
		||||
        enabled: true,
 | 
			
		||||
        name: 'variant',
 | 
			
		||||
        payload: {
 | 
			
		||||
            type: PayloadType.STRING,
 | 
			
		||||
            value: 'variant-A',
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const externalResolver = {
 | 
			
		||||
        isEnabled: () => true,
 | 
			
		||||
        getVariant: (name: string) => {
 | 
			
		||||
            if (name === 'extraFlag') {
 | 
			
		||||
                return variant;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const config = {
 | 
			
		||||
        flags: { extraFlag: undefined, someFlag: true, otherflag: false },
 | 
			
		||||
        externalResolver,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const resolver = new FlagResolver(config as IExperimentalOptions);
 | 
			
		||||
 | 
			
		||||
    expect(resolver.getVariant('someFlag' as IFlagKey)).toBe(undefined);
 | 
			
		||||
    expect(resolver.getVariant('otherFlag' as IFlagKey)).toBe(undefined);
 | 
			
		||||
    expect(resolver.getVariant('extraFlag' as IFlagKey)).toStrictEqual(variant);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should expose an helper to get variant value', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
        getVariantValue({
 | 
			
		||||
            enabled: true,
 | 
			
		||||
            name: 'variant',
 | 
			
		||||
            payload: {
 | 
			
		||||
                type: PayloadType.STRING,
 | 
			
		||||
                value: 'variant-A',
 | 
			
		||||
            },
 | 
			
		||||
        }),
 | 
			
		||||
    ).toBe('variant-A');
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
        getVariantValue({
 | 
			
		||||
            enabled: true,
 | 
			
		||||
            name: 'variant',
 | 
			
		||||
            payload: {
 | 
			
		||||
                type: PayloadType.JSON,
 | 
			
		||||
                value: `{"foo": "bar"}`,
 | 
			
		||||
            },
 | 
			
		||||
        }),
 | 
			
		||||
    ).toStrictEqual({
 | 
			
		||||
        foo: 'bar',
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { Variant, PayloadType } from 'unleash-client';
 | 
			
		||||
import {
 | 
			
		||||
    IExperimentalOptions,
 | 
			
		||||
    IExternalFlagResolver,
 | 
			
		||||
@ -6,6 +7,7 @@ import {
 | 
			
		||||
    IFlagResolver,
 | 
			
		||||
    IFlagKey,
 | 
			
		||||
} from '../types/experimental';
 | 
			
		||||
 | 
			
		||||
export default class FlagResolver implements IFlagResolver {
 | 
			
		||||
    private experiments: IFlags;
 | 
			
		||||
 | 
			
		||||
@ -20,20 +22,51 @@ export default class FlagResolver implements IFlagResolver {
 | 
			
		||||
        const flags: IFlags = { ...this.experiments };
 | 
			
		||||
 | 
			
		||||
        Object.keys(flags).forEach((flagName: IFlagKey) => {
 | 
			
		||||
            if (!this.experiments[flagName])
 | 
			
		||||
            if (!this.experiments[flagName]) {
 | 
			
		||||
                if (typeof flags[flagName] === 'boolean') {
 | 
			
		||||
                    flags[flagName] = this.externalResolver.isEnabled(
 | 
			
		||||
                        flagName,
 | 
			
		||||
                        context,
 | 
			
		||||
                    );
 | 
			
		||||
                } else {
 | 
			
		||||
                    flags[flagName] = this.externalResolver.getVariant(
 | 
			
		||||
                        flagName,
 | 
			
		||||
                        context,
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return flags;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isEnabled(expName: IFlagKey, context?: IFlagContext): boolean {
 | 
			
		||||
        if (this.experiments[expName]) {
 | 
			
		||||
            return true;
 | 
			
		||||
        const exp = this.experiments[expName];
 | 
			
		||||
        if (exp) {
 | 
			
		||||
            if (typeof exp === 'boolean') return exp;
 | 
			
		||||
            else return exp.enabled;
 | 
			
		||||
        }
 | 
			
		||||
        return this.externalResolver.isEnabled(expName, context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getVariant(expName: IFlagKey, context?: IFlagContext): Variant | undefined {
 | 
			
		||||
        const exp = this.experiments[expName];
 | 
			
		||||
        if (exp) {
 | 
			
		||||
            if (typeof exp === 'boolean') return undefined;
 | 
			
		||||
            else return exp;
 | 
			
		||||
        }
 | 
			
		||||
        return this.externalResolver.getVariant(expName, context);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getVariantValue = <T = string>(
 | 
			
		||||
    variant: Variant | undefined,
 | 
			
		||||
): T | undefined => {
 | 
			
		||||
    if (variant?.payload !== undefined) {
 | 
			
		||||
        if (variant.payload.type === PayloadType.JSON) {
 | 
			
		||||
            return JSON.parse(variant.payload.value) as T;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return variant.payload.value as T;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -6,10 +6,7 @@ import { ISegment } from 'lib/types/model';
 | 
			
		||||
import { serializeDates } from '../../lib/types/serialize-dates';
 | 
			
		||||
import { Operator } from './feature-evaluator/constraint';
 | 
			
		||||
import { FeatureInterface } from 'unleash-client/lib/feature';
 | 
			
		||||
 | 
			
		||||
enum PayloadType {
 | 
			
		||||
    STRING = 'string',
 | 
			
		||||
}
 | 
			
		||||
import { PayloadType } from 'unleash-client';
 | 
			
		||||
 | 
			
		||||
type NonEmptyList<T> = [T, ...T[]];
 | 
			
		||||
 | 
			
		||||
@ -24,7 +21,7 @@ export const mapFeaturesForClient = (
 | 
			
		||||
            ...variant,
 | 
			
		||||
            payload: variant.payload && {
 | 
			
		||||
                ...variant.payload,
 | 
			
		||||
                type: variant.payload.type as unknown as PayloadType,
 | 
			
		||||
                type: variant.payload.type as PayloadType,
 | 
			
		||||
            },
 | 
			
		||||
        })),
 | 
			
		||||
        project: feature.project,
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import { start } from './lib/server-impl';
 | 
			
		||||
import { createConfig } from './lib/create-config';
 | 
			
		||||
import { LogLevel } from './lib/logger';
 | 
			
		||||
import { ApiTokenType } from './lib/types/models/api-token';
 | 
			
		||||
import { PayloadType } from 'unleash-client';
 | 
			
		||||
 | 
			
		||||
process.nextTick(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
@ -40,6 +41,23 @@ process.nextTick(async () => {
 | 
			
		||||
                        responseTimeWithAppNameKillSwitch: false,
 | 
			
		||||
                        variantMetrics: true,
 | 
			
		||||
                        strategyImprovements: true,
 | 
			
		||||
                        messageBanner: {
 | 
			
		||||
                            name: 'message-banner',
 | 
			
		||||
                            enabled: true,
 | 
			
		||||
                            payload: {
 | 
			
		||||
                                type: PayloadType.JSON,
 | 
			
		||||
                                value: `{
 | 
			
		||||
                                    "message": "**New message banner!** Check out this new feature.",
 | 
			
		||||
                                    "variant": "secondary",
 | 
			
		||||
                                    "sticky": true,
 | 
			
		||||
                                    "link": "dialog",
 | 
			
		||||
                                    "linkText": "What is this?",
 | 
			
		||||
                                    "plausibleEvent": "message_banner",
 | 
			
		||||
                                    "dialog": "\\n## Message Banner 🎉\\n**New message banner!**\\n\\nCheck out this new feature:\\n\\n- Get the latest announcements\\n- Get warnings about your Unleash instance\\n\\nYou can read more about it on our newest [blog post](https://www.getunleash.io/blog).",
 | 
			
		||||
                                    "icon": "none"
 | 
			
		||||
                                }`,
 | 
			
		||||
                            },
 | 
			
		||||
                        },
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
                authentication: {
 | 
			
		||||
 | 
			
		||||
@ -3947,6 +3947,8 @@ Stats are divided into current and previous **windows**.
 | 
			
		||||
                  "type": {
 | 
			
		||||
                    "enum": [
 | 
			
		||||
                      "string",
 | 
			
		||||
                      "json",
 | 
			
		||||
                      "csv",
 | 
			
		||||
                    ],
 | 
			
		||||
                    "type": "string",
 | 
			
		||||
                  },
 | 
			
		||||
@ -4823,8 +4825,15 @@ Stats are divided into current and previous **windows**.
 | 
			
		||||
          },
 | 
			
		||||
          "flags": {
 | 
			
		||||
            "additionalProperties": {
 | 
			
		||||
              "anyOf": [
 | 
			
		||||
                {
 | 
			
		||||
                  "type": "boolean",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  "$ref": "#/components/schemas/variantFlagSchema",
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
            "type": "object",
 | 
			
		||||
          },
 | 
			
		||||
          "frontendApiOrigins": {
 | 
			
		||||
@ -5269,6 +5278,30 @@ Stats are divided into current and previous **windows**.
 | 
			
		||||
        ],
 | 
			
		||||
        "type": "object",
 | 
			
		||||
      },
 | 
			
		||||
      "variantFlagSchema": {
 | 
			
		||||
        "additionalProperties": false,
 | 
			
		||||
        "properties": {
 | 
			
		||||
          "enabled": {
 | 
			
		||||
            "type": "boolean",
 | 
			
		||||
          },
 | 
			
		||||
          "name": {
 | 
			
		||||
            "type": "string",
 | 
			
		||||
          },
 | 
			
		||||
          "payload": {
 | 
			
		||||
            "additionalProperties": false,
 | 
			
		||||
            "properties": {
 | 
			
		||||
              "type": {
 | 
			
		||||
                "type": "string",
 | 
			
		||||
              },
 | 
			
		||||
              "value": {
 | 
			
		||||
                "type": "string",
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
            "type": "object",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        "type": "object",
 | 
			
		||||
      },
 | 
			
		||||
      "variantSchema": {
 | 
			
		||||
        "additionalProperties": false,
 | 
			
		||||
        "properties": {
 | 
			
		||||
 | 
			
		||||
@ -7210,10 +7210,10 @@ universalify@^0.1.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
 | 
			
		||||
  integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
 | 
			
		||||
 | 
			
		||||
unleash-client@3.18.1:
 | 
			
		||||
  version "3.18.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/unleash-client/-/unleash-client-3.18.1.tgz#d9e928f3cf0c11dafce27bae298b183b28615b4d"
 | 
			
		||||
  integrity sha512-fWVxeas4XzXkPPkTxLr2MKVvN4DUkYDVOKDG9zlnqQnmWvZQjLnRqOCOvf/vFkd4qJj+4fSWIYKTrMYQIpNUKw==
 | 
			
		||||
unleash-client@3.20.0:
 | 
			
		||||
  version "3.20.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/unleash-client/-/unleash-client-3.20.0.tgz#1a6deb0e803eed0d0cefbed1bac17f3c3d3b0143"
 | 
			
		||||
  integrity sha512-CXseZTHH+lfT3qZY7nufpPKbnNcWvdt61Pgc313spFnQBV63r24fhMmwvQcltc+pp2z/14p2mM6iq11R2PYw3g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    ip "^1.1.8"
 | 
			
		||||
    make-fetch-happen "^10.2.1"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user