From db61a8a40cd5048f90ea053b869277265f8cc500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Thu, 18 May 2023 09:38:59 +0100 Subject: [PATCH] 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~~ (https://github.com/Unleash/unleash/pull/3788/commits/50ccf608937ace7f0d9aecefe83084ed18978a3c); - ~~Enterprise needs a PR that will follow soon~~; ![image](https://github.com/Unleash/unleash/assets/14320932/034ff64f-3020-4ed0-863b-ed1fd9190430) --- .../common/MessageBanner/MessageBanner.tsx | 87 +++++++------------ frontend/src/hooks/useVariant.ts | 10 +++ frontend/src/interfaces/uiConfig.ts | 3 +- frontend/src/utils/variants.ts | 28 ++++++ package.json | 2 +- .../__snapshots__/create-config.test.ts.snap | 20 ++++- src/lib/openapi/index.ts | 2 + src/lib/openapi/meta-schema-rules.test.ts | 2 + src/lib/openapi/spec/index.ts | 1 + src/lib/openapi/spec/proxy-feature-schema.ts | 6 +- src/lib/openapi/spec/ui-config-schema.ts | 11 ++- src/lib/openapi/spec/variant-flag-schema.ts | 30 +++++++ src/lib/types/experimental.ts | 55 ++++++++++-- src/lib/util/feature-evaluator/variant.ts | 5 +- src/lib/util/flag-resolver.test.ts | 64 +++++++++++++- src/lib/util/flag-resolver.ts | 47 ++++++++-- src/lib/util/offline-unleash-client.ts | 7 +- src/server-dev.ts | 18 ++++ .../__snapshots__/openapi.e2e.test.ts.snap | 35 +++++++- yarn.lock | 8 +- 20 files changed, 350 insertions(+), 91 deletions(-) create mode 100644 frontend/src/hooks/useVariant.ts create mode 100644 frontend/src/utils/variants.ts create mode 100644 src/lib/openapi/spec/variant-flag-schema.ts diff --git a/frontend/src/component/common/MessageBanner/MessageBanner.tsx b/frontend/src/component/common/MessageBanner/MessageBanner.tsx index 36f5841f08..a2c0416055 100644 --- a/frontend/src/component/common/MessageBanner/MessageBanner.tsx +++ b/frontend/src/component/common/MessageBanner/MessageBanner.tsx @@ -6,23 +6,30 @@ 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', - zIndex: 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - padding: theme.spacing(1), - gap: theme.spacing(1), - borderBottom: '1px solid', - borderColor: theme.palette[variant].border, - background: theme.palette[variant].light, - color: theme.palette[variant].dark, - fontSize: theme.fontSizes.smallBody, -})); + shouldForwardProp: prop => prop !== 'variant' && prop !== 'sticky', +})<{ variant: BannerVariant; sticky?: boolean }>( + ({ theme, variant, sticky }) => ({ + position: sticky ? 'sticky' : 'relative', + zIndex: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: theme.spacing(1), + gap: theme.spacing(1), + borderBottom: '1px solid', + borderColor: theme.palette[variant].border, + 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](https://www.getunleash.io/logos/unleash_pos.svg) -## 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( + 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 ( - + diff --git a/frontend/src/hooks/useVariant.ts b/frontend/src/hooks/useVariant.ts new file mode 100644 index 0000000000..d9a0e73e80 --- /dev/null +++ b/frontend/src/hooks/useVariant.ts @@ -0,0 +1,10 @@ +import { useMemo } from 'react'; +import { Variant, getVariantValue } from 'utils/variants'; + +export const useVariant = (variant?: Variant) => { + return useMemo(() => { + if (variant?.enabled) { + return getVariantValue(variant); + } + }, [variant]); +}; diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index 1dd8c05e66..7ad2fef0b8 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -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; diff --git a/frontend/src/utils/variants.ts b/frontend/src/utils/variants.ts new file mode 100644 index 0000000000..4063b5bb70 --- /dev/null +++ b/frontend/src/utils/variants.ts @@ -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 = ( + 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; + } +}; diff --git a/package.json b/package.json index e089fbcb23..37d1bcdfc1 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 5b300af392..09ec2bbae2 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -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], }, }, diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index 5d5d6b480f..ad87046618 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -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, diff --git a/src/lib/openapi/meta-schema-rules.test.ts b/src/lib/openapi/meta-schema-rules.test.ts index 634e801274..ef807b3e62 100644 --- a/src/lib/openapi/meta-schema-rules.test.ts +++ b/src/lib/openapi/meta-schema-rules.test.ts @@ -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', diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index dfa209993e..3cb523723f 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -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'; diff --git a/src/lib/openapi/spec/proxy-feature-schema.ts b/src/lib/openapi/spec/proxy-feature-schema.ts index 8aaa7d5abd..003437a598 100644 --- a/src/lib/openapi/spec/proxy-feature-schema.ts +++ b/src/lib/openapi/spec/proxy-feature-schema.ts @@ -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' }, }, }, diff --git a/src/lib/openapi/spec/ui-config-schema.ts b/src/lib/openapi/spec/ui-config-schema.ts index be9f264ef5..38f0acc053 100644 --- a/src/lib/openapi/spec/ui-config-schema.ts +++ b/src/lib/openapi/spec/ui-config-schema.ts @@ -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,7 +53,14 @@ export const uiConfigSchema = { flags: { type: 'object', additionalProperties: { - type: 'boolean', + anyOf: [ + { + type: 'boolean', + }, + { + $ref: '#/components/schemas/variantFlagSchema', + }, + ], }, }, links: { @@ -79,6 +87,7 @@ export const uiConfigSchema = { components: { schemas: { versionSchema, + variantFlagSchema, }, }, } as const; diff --git a/src/lib/openapi/spec/variant-flag-schema.ts b/src/lib/openapi/spec/variant-flag-schema.ts new file mode 100644 index 0000000000..ed46d7b9cd --- /dev/null +++ b/src/lib/openapi/spec/variant-flag-schema.ts @@ -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; diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 57507532d0..3954998e0e 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -1,9 +1,29 @@ +import { Variant, PayloadType } from 'unleash-client'; import { parseEnvVarBoolean } from '../util'; -export type IFlags = Partial; -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( - process.env.UNLEASH_EXPERIMENTAL_MESSAGE_BANNER, - false, - ), + 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; } diff --git a/src/lib/util/feature-evaluator/variant.ts b/src/lib/util/feature-evaluator/variant.ts index ccb791f063..10bb497ac7 100644 --- a/src/lib/util/feature-evaluator/variant.ts +++ b/src/lib/util/feature-evaluator/variant.ts @@ -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; diff --git a/src/lib/util/flag-resolver.test.ts b/src/lib/util/flag-resolver.test.ts index b4cf860e36..de9ac4633b 100644 --- a/src/lib/util/flag-resolver.test.ts +++ b/src/lib/util/flag-resolver.test.ts @@ -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', + }); +}); diff --git a/src/lib/util/flag-resolver.ts b/src/lib/util/flag-resolver.ts index 63fdafc84f..180235a684 100644 --- a/src/lib/util/flag-resolver.ts +++ b/src/lib/util/flag-resolver.ts @@ -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]) - flags[flagName] = this.externalResolver.isEnabled( - flagName, - context, - ); + 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 = ( + 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; + } +}; diff --git a/src/lib/util/offline-unleash-client.ts b/src/lib/util/offline-unleash-client.ts index 6fa0a0e57a..c17a19b947 100644 --- a/src/lib/util/offline-unleash-client.ts +++ b/src/lib/util/offline-unleash-client.ts @@ -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[]]; @@ -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, diff --git a/src/server-dev.ts b/src/server-dev.ts index 44bc204350..c4a757d70d 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -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": "![Message Banner](https://www.getunleash.io/logos/unleash_pos.svg)\\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: { diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index 7e4dd6fc3e..7dd086925c 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -3947,6 +3947,8 @@ Stats are divided into current and previous **windows**. "type": { "enum": [ "string", + "json", + "csv", ], "type": "string", }, @@ -4823,7 +4825,14 @@ Stats are divided into current and previous **windows**. }, "flags": { "additionalProperties": { - "type": "boolean", + "anyOf": [ + { + "type": "boolean", + }, + { + "$ref": "#/components/schemas/variantFlagSchema", + }, + ], }, "type": "object", }, @@ -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": { diff --git a/yarn.lock b/yarn.lock index bb5344f62d..9ca4951b14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"