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`,