diff --git a/frontend/src/component/banners/OutdatedSdksBanner/OutdatedSdksBanner.test.tsx b/frontend/src/component/banners/OutdatedSdksBanner/OutdatedSdksBanner.test.tsx
new file mode 100644
index 0000000000..398ce42197
--- /dev/null
+++ b/frontend/src/component/banners/OutdatedSdksBanner/OutdatedSdksBanner.test.tsx
@@ -0,0 +1,36 @@
+import { screen } from '@testing-library/react';
+import { render } from 'utils/testRenderer';
+import { testServerRoute, testServerSetup } from 'utils/testServer';
+import { OutdatedSdksSchema } from 'openapi';
+import { OutdatedSdksBanner } from './OutdatedSdksBanner';
+
+const server = testServerSetup();
+
+const setupApi = (outdatedSdks: OutdatedSdksSchema) => {
+ testServerRoute(server, '/api/admin/metrics/sdks/outdated', outdatedSdks);
+ testServerRoute(server, '/api/admin/ui-config', {
+ flags: {
+ outdatedSdksBanner: true,
+ },
+ });
+};
+
+test('Show outdated SDKs and apps using them', async () => {
+ setupApi({
+ sdks: [
+ {
+ sdkVersion: 'unleash-node-client:3.2.1',
+ applications: ['application1', 'application2'],
+ },
+ ],
+ });
+ render();
+
+ const link = await screen.findByText('Please update those versions');
+
+ link.click();
+
+ await screen.findByText('unleash-node-client:3.2.1');
+ await screen.findByText('application1');
+ await screen.findByText('application2');
+});
diff --git a/frontend/src/component/banners/OutdatedSdksBanner/OutdatedSdksBanner.tsx b/frontend/src/component/banners/OutdatedSdksBanner/OutdatedSdksBanner.tsx
index b3f45a2a1d..e749a2e7eb 100644
--- a/frontend/src/component/banners/OutdatedSdksBanner/OutdatedSdksBanner.tsx
+++ b/frontend/src/component/banners/OutdatedSdksBanner/OutdatedSdksBanner.tsx
@@ -1,21 +1,48 @@
import { ConditionallyRender } from '../../common/ConditionallyRender/ConditionallyRender';
import { Banner } from '../Banner/Banner';
-import { IBanner } from '../../../interfaces/banner';
+import { IBanner } from 'interfaces/banner';
+import { useOutdatedSdks } from 'hooks/api/getters/useOutdatedSdks/useOutdatedSdks';
+import { useUiFlag } from 'hooks/useUiFlag';
+import { Link } from 'react-router-dom';
+import { styled } from '@mui/material';
+
+const StyledList = styled('ul')({ margin: 0 });
export const OutdatedSdksBanner = () => {
- const displayOutdatedSdksBanner = false;
+ const {
+ data: { sdks },
+ } = useOutdatedSdks();
+ const flagEnabled = useUiFlag('outdatedSdksBanner');
+
const outdatedSdksBanner: IBanner = {
message: `We noticed that you're using outdated SDKs. `,
variant: 'warning',
link: 'dialog',
linkText: 'Please update those versions',
dialogTitle: 'Outdated SDKs',
- dialog:
Outdated SDKs
,
+ dialog: (
+ <>
+ {sdks.map((item) => (
+
+ {item.sdkVersion}
+
+ {item.applications.map((application) => (
+
+
+ {application}
+
+
+ ))}
+
+
+ ))}
+ >
+ ),
};
return (
<>
0}
show={}
/>
>
diff --git a/frontend/src/hooks/api/getters/useApiGetter/useApiGetter.ts b/frontend/src/hooks/api/getters/useApiGetter/useApiGetter.ts
index 93a9548190..9c78032d3c 100644
--- a/frontend/src/hooks/api/getters/useApiGetter/useApiGetter.ts
+++ b/frontend/src/hooks/api/getters/useApiGetter/useApiGetter.ts
@@ -1,5 +1,6 @@
import useSWR, { SWRConfiguration, Key } from 'swr';
import { useCallback } from 'react';
+import handleErrorResponses from '../httpErrorResponseHandler';
interface IUseApiGetterOutput {
data?: T;
@@ -26,3 +27,9 @@ export const useApiGetter = (
loading: !error && !data,
};
};
+
+export const fetcher = (path: string, errorTarget: string) => {
+ return fetch(path)
+ .then(handleErrorResponses(errorTarget))
+ .then((res) => res.json());
+};
diff --git a/frontend/src/hooks/api/getters/useOutdatedSdks/useOutdatedSdks.ts b/frontend/src/hooks/api/getters/useOutdatedSdks/useOutdatedSdks.ts
new file mode 100644
index 0000000000..979e94a740
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useOutdatedSdks/useOutdatedSdks.ts
@@ -0,0 +1,14 @@
+import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter';
+import { OutdatedSdksSchema } from '../../../../openapi';
+
+const PATH = 'api/admin/metrics/sdks/outdated';
+
+export const useOutdatedSdks = () => {
+ const { data, refetch, loading, error } = useApiGetter(
+ PATH,
+ () => fetcher(PATH, 'Outdated SDKs'),
+ { refreshInterval: 60 * 1000 },
+ );
+
+ return { data: data || { sdks: [] }, refetch, error };
+};
diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts
index 34c8f6bdd0..0c4a1d6308 100644
--- a/frontend/src/interfaces/uiConfig.ts
+++ b/frontend/src/interfaces/uiConfig.ts
@@ -79,6 +79,7 @@ export type UiFlags = {
featureSearchFeedbackPosting?: boolean;
userAccessUIEnabled?: boolean;
sdkReporting?: boolean;
+ outdatedSdksBanner?: boolean;
};
export interface IVersionInfo {
diff --git a/frontend/src/openapi/models/getOutdatedSdks404.ts b/frontend/src/openapi/models/getOutdatedSdks404.ts
new file mode 100644
index 0000000000..a88fb9b8c3
--- /dev/null
+++ b/frontend/src/openapi/models/getOutdatedSdks404.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by Orval
+ * Do not edit manually.
+ * See `gen:api` script in package.json
+ */
+
+export type GetOutdatedSdks404 = {
+ /** The ID of the error instance */
+ id?: string;
+ /** A description of what went wrong. */
+ message?: string;
+ /** The name of the error kind */
+ name?: string;
+};
diff --git a/frontend/src/openapi/models/index.ts b/frontend/src/openapi/models/index.ts
index eafe8e59b6..7cce221cf7 100644
--- a/frontend/src/openapi/models/index.ts
+++ b/frontend/src/openapi/models/index.ts
@@ -648,6 +648,7 @@ export * from './getMe401';
export * from './getOidcSettings400';
export * from './getOidcSettings401';
export * from './getOidcSettings403';
+export * from './getOutdatedSdks404';
export * from './getPats401';
export * from './getPats403';
export * from './getPats404';
@@ -789,6 +790,8 @@ export * from './notificationsSchemaItemNotificationType';
export * from './oidcSettingsSchema';
export * from './oidcSettingsSchemaDefaultRootRole';
export * from './oidcSettingsSchemaIdTokenSigningAlgorithm';
+export * from './outdatedSdksSchema';
+export * from './outdatedSdksSchemaSdksItem';
export * from './overrideSchema';
export * from './overwriteEnvironmentFeatureVariants400';
export * from './overwriteEnvironmentFeatureVariants401';
@@ -1044,6 +1047,7 @@ export * from './toggleEnvironmentOff404';
export * from './toggleEnvironmentOn401';
export * from './toggleEnvironmentOn403';
export * from './toggleEnvironmentOn404';
+export * from './toggleFeatureActionSchema';
export * from './toggleFeatureEnvironmentOff400';
export * from './toggleFeatureEnvironmentOff401';
export * from './toggleFeatureEnvironmentOff403';
diff --git a/frontend/src/openapi/models/outdatedSdksSchema.ts b/frontend/src/openapi/models/outdatedSdksSchema.ts
new file mode 100644
index 0000000000..6ec55a0616
--- /dev/null
+++ b/frontend/src/openapi/models/outdatedSdksSchema.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by Orval
+ * Do not edit manually.
+ * See `gen:api` script in package.json
+ */
+import type { OutdatedSdksSchemaSdksItem } from './outdatedSdksSchemaSdksItem';
+
+/**
+ * Data about outdated SDKs that should be upgraded.
+ */
+export interface OutdatedSdksSchema {
+ /** A list of SDKs */
+ sdks: OutdatedSdksSchemaSdksItem[];
+}
diff --git a/frontend/src/openapi/models/outdatedSdksSchemaSdksItem.ts b/frontend/src/openapi/models/outdatedSdksSchemaSdksItem.ts
new file mode 100644
index 0000000000..3fbacc2dff
--- /dev/null
+++ b/frontend/src/openapi/models/outdatedSdksSchemaSdksItem.ts
@@ -0,0 +1,12 @@
+/**
+ * Generated by Orval
+ * Do not edit manually.
+ * See `gen:api` script in package.json
+ */
+
+export type OutdatedSdksSchemaSdksItem = {
+ /** A list of applications using the SDK version */
+ applications: string[];
+ /** An outdated SDK version identifier. Usually formatted as "unleash-client-:" */
+ sdkVersion: string;
+};
diff --git a/frontend/src/openapi/models/toggleFeatureActionSchema.ts b/frontend/src/openapi/models/toggleFeatureActionSchema.ts
new file mode 100644
index 0000000000..ec58b8bb4d
--- /dev/null
+++ b/frontend/src/openapi/models/toggleFeatureActionSchema.ts
@@ -0,0 +1,17 @@
+/**
+ * Generated by Orval
+ * Do not edit manually.
+ * See `gen:api` script in package.json
+ */
+
+/**
+ * Input data required for the action
+ */
+export interface ToggleFeatureActionSchema {
+ /** The environment we want to target */
+ environment: string;
+ /** The name of the feature we want to target */
+ featureName: string;
+ /** The project where the feature is located */
+ project: string;
+}
diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap
index c44430d36a..aa2a5d490c 100644
--- a/src/lib/__snapshots__/create-config.test.ts.snap
+++ b/src/lib/__snapshots__/create-config.test.ts.snap
@@ -130,6 +130,7 @@ exports[`should create default config 1`] = `
},
"migrationLock": true,
"newStrategyConfigurationFeedback": false,
+ "outdatedSdksBanner": false,
"personalAccessTokensKillSwitch": false,
"proPlanAutoCharge": false,
"queryMissingTokens": false,
diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts
index 8bc7cba736..3912973a94 100644
--- a/src/lib/types/experimental.ts
+++ b/src/lib/types/experimental.ts
@@ -51,6 +51,7 @@ export type IFlagKey =
| 'disableUpdateMaxRevisionId'
| 'disablePublishUnannouncedEvents'
| 'sdkReporting'
+ | 'outdatedSdksBanner'
| 'responseTimeMetricsFix'
| 'scimApi'
| 'displayEdgeBanner'
@@ -203,6 +204,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_SDK_REPORTING,
false,
),
+ outdatedSdksBanner: parseEnvVarBoolean(
+ process.env.UNLEASH_EXPERIMENTAL_OUTDATED_SDKS_BANNER,
+ false,
+ ),
feedbackComments: {
name: 'feedbackComments',
enabled: parseEnvVarBoolean(