diff --git a/frontend/index.html b/frontend/index.html index 5f9089c542..1b538160b3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,6 +7,7 @@ + Unleash diff --git a/frontend/package.json b/frontend/package.json index d99d07ac8c..d9a0834013 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -69,6 +69,7 @@ "@types/uuid": "^9.0.0", "@uiw/codemirror-theme-duotone": "4.23.10", "@uiw/react-codemirror": "4.23.10", + "@unleash/proxy-client-react": "^5.0.0", "@vitejs/plugin-react": "4.3.4", "cartesian": "^1.0.1", "chart.js": "3.9.1", @@ -118,6 +119,7 @@ "swr": "2.3.3", "tss-react": "4.9.15", "typescript": "5.4.5", + "unleash-proxy-client": "^3.7.3", "use-query-params": "^2.2.1", "vanilla-jsoneditor": "^0.23.0", "vite": "5.4.16", diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx index 2ecf33090d..4742db4828 100644 --- a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx @@ -28,8 +28,9 @@ import { ReactComponent as CelebatoryUnleashLogo } from 'assets/img/unleashHolid import { ReactComponent as CelebatoryUnleashLogoWhite } from 'assets/img/unleashHolidayDark.svg'; import { ReactComponent as LogoOnlyWhite } from 'assets/img/logo.svg'; import { ReactComponent as LogoOnly } from 'assets/img/logoDark.svg'; -import { useUiFlag } from 'hooks/useUiFlag'; import { Link } from 'react-router-dom'; +import { useFlag } from '@unleash/proxy-client-react'; +import { useUiFlag } from 'hooks/useUiFlag'; export const MobileNavigationSidebar: FC<{ onClick: () => void; @@ -110,7 +111,7 @@ export const NavigationSidebar: FC<{ NewInUnleash?: typeof NewInUnleash }> = ({ NewInUnleash, }) => { const { routes } = useRoutes(); - const celebatoryUnleash = useUiFlag('celebrateUnleash'); + const celebrateUnleashFrontend = useFlag('celebrateUnleashFrontend'); const [mode, setMode] = useNavigationMode(); const [expanded, changeExpanded] = useExpanded<'configure' | 'admin'>(); @@ -138,7 +139,7 @@ export const NavigationSidebar: FC<{ NewInUnleash?: typeof NewInUnleash }> = ({ } elseShow={ @@ -147,7 +148,7 @@ export const NavigationSidebar: FC<{ NewInUnleash?: typeof NewInUnleash }> = ({ } lightmode={ } elseShow={ diff --git a/frontend/src/component/providers/UnleashFlagProvider/UnleashFlagProvider.tsx b/frontend/src/component/providers/UnleashFlagProvider/UnleashFlagProvider.tsx new file mode 100644 index 0000000000..91c8e1c2c3 --- /dev/null +++ b/frontend/src/component/providers/UnleashFlagProvider/UnleashFlagProvider.tsx @@ -0,0 +1,56 @@ +import type React from 'react'; +import { type FC, useEffect } from 'react'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import FlagProvider, { UnleashClient } from '@unleash/proxy-client-react'; + +const UNLEASH_API = 'https://hosted.edge.getunleash.io/api/frontend'; +const DEV_TOKEN = ''; + +let client: UnleashClient; +let token: string; +let started: boolean = false; + +export const UnleashFlagProvider: FC<{ children?: React.ReactNode }> = ({ + children, +}) => { + const getUnleashFrontendToken = (): string => { + const el = document.querySelector( + 'meta[name="unleashToken"]', + ); + + const content = el?.content ?? '::unleashToken::'; + return content === '::unleashToken::' ? DEV_TOKEN : content; + }; + + // We only want to create a single client. + if (!client) { + token = getUnleashFrontendToken(); + + client = new UnleashClient({ + url: UNLEASH_API, + clientKey: token || 'offline', + refreshInterval: 1, + appName: 'Unleash Cloud UI', + }); + } + + const { uiConfig } = useUiConfig(); + + useEffect(() => { + if (uiConfig.unleashContext && token) { + client.updateContext(uiConfig.unleashContext); + if (!started) { + started = true; + client.start(); + } + } else { + // nothing + } + }, [uiConfig.unleashContext]); + + return ( + + {children} + + ); +}; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 031a9f6c6e..7277d05ef4 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -22,6 +22,7 @@ import { Error as LayoutError } from './component/layout/Error/Error'; import { ErrorBoundary } from 'react-error-boundary'; import { useRecordUIErrorApi } from 'hooks/api/actions/useRecordUIErrorApi/useRecordUiErrorApi'; import { HighlightProvider } from 'component/common/Highlight/HighlightProvider'; +import { UnleashFlagProvider } from 'component/providers/UnleashFlagProvider/UnleashFlagProvider'; window.global ||= window; @@ -50,23 +51,25 @@ const ApplicationRoot = () => { - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index 8ae21784df..829b977855 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -1,6 +1,8 @@ import type { ReactNode } from 'react'; import type { Variant } from 'utils/variants'; import type { ResourceLimitsSchema } from '../openapi'; +import {} from '@unleash/proxy-client-react/dist/FlagContext'; +import type { IMutableContext } from 'unleash-proxy-client'; export interface IUiConfig { authenticationType?: string; @@ -34,6 +36,7 @@ export interface IUiConfig { oidcConfiguredThroughEnv?: boolean; samlConfiguredThroughEnv?: boolean; maxSessionsCount?: number; + unleashContext?: IMutableContext; } export interface IProclamationToast { diff --git a/frontend/yarn.lock b/frontend/yarn.lock index d01d557080..b4c25ce3cf 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3371,6 +3371,15 @@ __metadata: languageName: node linkType: hard +"@unleash/proxy-client-react@npm:^5.0.0": + version: 5.0.0 + resolution: "@unleash/proxy-client-react@npm:5.0.0" + peerDependencies: + unleash-proxy-client: ^3.7.3 + checksum: 10c0/cf8d227a2db06c5ca09be4be45885306b71733540ace3980ff3794bfaa22c6b38a15446e46ca8880478c018d8ecd0f14e14cb203a6c3ba9b901853db0a9890ba + languageName: node + linkType: hard + "@vitejs/plugin-react@npm:4.3.4": version: 4.3.4 resolution: "@vitejs/plugin-react@npm:4.3.4" @@ -9609,6 +9618,13 @@ __metadata: languageName: node linkType: hard +"tiny-emitter@npm:^2.1.0": + version: 2.1.0 + resolution: "tiny-emitter@npm:2.1.0" + checksum: 10c0/459c0bd6e636e80909898220eb390e1cba2b15c430b7b06cec6ac29d87acd29ef618b9b32532283af749f5d37af3534d0e3bde29fdf6bcefbf122784333c953d + languageName: node + linkType: hard + "tinybench@npm:^2.9.0": version: 2.9.0 resolution: "tinybench@npm:2.9.0" @@ -10123,6 +10139,7 @@ __metadata: "@types/uuid": "npm:^9.0.0" "@uiw/codemirror-theme-duotone": "npm:4.23.10" "@uiw/react-codemirror": "npm:4.23.10" + "@unleash/proxy-client-react": "npm:^5.0.0" "@vitejs/plugin-react": "npm:4.3.4" cartesian: "npm:^1.0.1" chart.js: "npm:3.9.1" @@ -10173,6 +10190,7 @@ __metadata: swr: "npm:2.3.3" tss-react: "npm:4.9.15" typescript: "npm:5.4.5" + unleash-proxy-client: "npm:^3.7.3" use-query-params: "npm:^2.2.1" vanilla-jsoneditor: "npm:^0.23.0" vite: "npm:5.4.16" @@ -10184,6 +10202,16 @@ __metadata: languageName: unknown linkType: soft +"unleash-proxy-client@npm:^3.7.3": + version: 3.7.3 + resolution: "unleash-proxy-client@npm:3.7.3" + dependencies: + tiny-emitter: "npm:^2.1.0" + uuid: "npm:^9.0.1" + checksum: 10c0/3a061d4e3587325046fea0133fe405fef143dbcfdd6ed20c54200b46a22bf49acdccb6dcc0b250400a9ace2350b0065f856731a5712598d27c1e9266a141f559 + languageName: node + linkType: hard + "untildify@npm:^4.0.0": version: 4.0.0 resolution: "untildify@npm:4.0.0" @@ -10298,6 +10326,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^9.0.1": + version: 9.0.1 + resolution: "uuid@npm:9.0.1" + bin: + uuid: dist/bin/uuid + checksum: 10c0/1607dd32ac7fc22f2d8f77051e6a64845c9bce5cd3dd8aa0070c074ec73e666a1f63c7b4e0f4bf2bc8b9d59dc85a15e17807446d9d2b17c8485fbc2147b27f9b + languageName: node + linkType: hard + "uvu@npm:^0.5.0": version: 0.5.6 resolution: "uvu@npm:0.5.6" diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index d7b451441c..126396e94a 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -158,6 +158,7 @@ exports[`should create default config 1`] = ` "ui": { "environment": "Open Source", }, + "unleashFrontendToken": undefined, "userInactivityThresholdInDays": 180, "versionCheck": { "enable": true, diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index 01d928833f..730724b47d 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -751,6 +751,9 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { const openAIAPIKey = process.env.OPENAI_API_KEY; + const unleashFrontendToken = + options.unleashFrontendToken || process.env.UNLEASH_FRONTEND_TOKEN; + const defaultDaysToBeConsideredInactive = 180; const userInactivityThresholdInDays = options.userInactivityThresholdInDays ?? @@ -801,6 +804,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { openAIAPIKey, userInactivityThresholdInDays, buildDate: process.env.BUILD_DATE, + unleashFrontendToken, }; } diff --git a/src/lib/middleware/response-time-metrics.test.ts b/src/lib/middleware/response-time-metrics.test.ts index db4456e50b..46fc9884db 100644 --- a/src/lib/middleware/response-time-metrics.test.ts +++ b/src/lib/middleware/response-time-metrics.test.ts @@ -31,6 +31,7 @@ const flagResolver = { isEnabled: jest.fn(), getAll: jest.fn(), getVariant: jest.fn(), + getStaticContext: jest.fn(), }; // Make sure it's always cleaned up diff --git a/src/lib/openapi/spec/ui-config-schema.ts b/src/lib/openapi/spec/ui-config-schema.ts index 8a13059841..c0baf94c78 100644 --- a/src/lib/openapi/spec/ui-config-schema.ts +++ b/src/lib/openapi/spec/ui-config-schema.ts @@ -191,6 +191,11 @@ export const uiConfigSchema = { description: 'The maximum number of sessions that a user has.', example: 10, }, + unleashContext: { + type: 'object', + description: + 'The context object used to configure the Unleash instance.', + }, }, components: { schemas: { diff --git a/src/lib/routes/admin-api/config.ts b/src/lib/routes/admin-api/config.ts index e9cfbd36d2..a5205bebee 100644 --- a/src/lib/routes/admin-api/config.ts +++ b/src/lib/routes/admin-api/config.ts @@ -174,6 +174,12 @@ class ConfigController extends Controller { ...expFlags, }; + const unleashContext = { + ...this.flagResolver.getStaticContext(), //clientId etc. + email: req.user.email, + userId: req.user.id, + }; + const response: UiConfigSchema = { ...this.config.ui, flags, @@ -192,6 +198,7 @@ class ConfigController extends Controller { maintenanceMode, feedbackUriPath: this.config.feedbackUriPath, maxSessionsCount, + unleashContext: unleashContext, }; this.openApiService.respondWithValidation( diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 7c361c226c..e4b56ab478 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -352,6 +352,7 @@ export interface IFlagResolver { getAll: (context?: IFlagContext) => IFlags; isEnabled: (expName: IFlagKey, context?: IFlagContext) => boolean; getVariant: (expName: IFlagKey, context?: IFlagContext) => Variant; + getStaticContext: () => IFlagContext; } export interface IExternalFlagResolver { diff --git a/src/lib/types/option.ts b/src/lib/types/option.ts index fffe00539d..a14f27f949 100644 --- a/src/lib/types/option.ts +++ b/src/lib/types/option.ts @@ -4,6 +4,7 @@ import type { LogLevel, LogProvider } from '../logger'; import type { ILegacyApiTokenCreate } from './models/api-token'; import type { IExperimentalOptions, + IFlagContext, IFlagResolver, IFlags, } from './experimental'; @@ -165,6 +166,7 @@ export interface IUnleashOptions { > >; userInactivityThresholdInDays?: number; + unleashFrontendToken?: string; } export interface IEmailOption { @@ -198,6 +200,8 @@ export interface IUIConfig { title: string; }[]; flags?: IFlags; + unleashToken?: string; + unleashContext?: IFlagContext; } export interface ICspDomainOptions { @@ -287,4 +291,5 @@ export interface IUnleashConfig { openAIAPIKey?: string; userInactivityThresholdInDays: number; buildDate?: string; + unleashFrontendToken?: string; } diff --git a/src/lib/util/flag-resolver.ts b/src/lib/util/flag-resolver.ts index ad20870385..e119d1abdc 100644 --- a/src/lib/util/flag-resolver.ts +++ b/src/lib/util/flag-resolver.ts @@ -61,6 +61,10 @@ export default class FlagResolver implements IFlagResolver { } return this.externalResolver.getVariant(expName, context); } + + getStaticContext(): IFlagContext { + return {}; + } } export const getVariantValue = ( diff --git a/src/lib/util/load-index-html.ts b/src/lib/util/load-index-html.ts index 531841e052..d086e3d8fc 100644 --- a/src/lib/util/load-index-html.ts +++ b/src/lib/util/load-index-html.ts @@ -10,6 +10,7 @@ export async function loadIndexHTML( ): Promise { const { cdnPrefix, baseUriPath = '' } = config.server; const uiFlags = encodeURI(JSON.stringify(config.ui.flags || '{}')); + const unleashToken = config.unleashFrontendToken; let indexHTML: string; if (cdnPrefix) { @@ -23,5 +24,11 @@ export async function loadIndexHTML( .toString(); } - return rewriteHTML(indexHTML, baseUriPath, cdnPrefix, uiFlags); + return rewriteHTML( + indexHTML, + baseUriPath, + cdnPrefix, + uiFlags, + unleashToken, + ); } diff --git a/src/lib/util/rewriteHTML.ts b/src/lib/util/rewriteHTML.ts index 58de0c11c4..726ca7b924 100644 --- a/src/lib/util/rewriteHTML.ts +++ b/src/lib/util/rewriteHTML.ts @@ -3,6 +3,7 @@ export const rewriteHTML = ( rewriteValue: string, cdnPrefix?: string, uiFlags?: string, + unleashToken?: string, ): string => { let result = input; result = result.replace(/::baseUriPath::/gi, rewriteValue); @@ -13,6 +14,8 @@ export const rewriteHTML = ( result = result.replace(/::uiFlags::/gi, uiFlags || '{}'); + result = result.replace(/::unleashToken::/gi, unleashToken || ''); + result = result.replace( /\/static/gi, `${cdnPrefix || rewriteValue}/static`,