mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
task: Add banner encouraging edge upgrade (#6018)
Only triggers if there is any rows in client instances that have sdk_version: unleash-edge with version < 17.0.0 The function that checks this memoizes the check for 10 minutes to avoid scanning the client instances table too often.
This commit is contained in:
parent
3acdfc2cf4
commit
17d826ddf4
@ -21,6 +21,7 @@ import { styled } from '@mui/material';
|
|||||||
import { InitialRedirect } from './InitialRedirect';
|
import { InitialRedirect } from './InitialRedirect';
|
||||||
import { InternalBanners } from './banners/internalBanners/InternalBanners';
|
import { InternalBanners } from './banners/internalBanners/InternalBanners';
|
||||||
import { ExternalBanners } from './banners/externalBanners/ExternalBanners';
|
import { ExternalBanners } from './banners/externalBanners/ExternalBanners';
|
||||||
|
import { EdgeUpgradeBanner } from './banners/EdgeUpgradeBanner/EdgeUpgradeBanner';
|
||||||
import { LicenseBanner } from './banners/internalBanners/LicenseBanner';
|
import { LicenseBanner } from './banners/internalBanners/LicenseBanner';
|
||||||
import { Demo } from './demo/Demo';
|
import { Demo } from './demo/Demo';
|
||||||
|
|
||||||
@ -68,6 +69,7 @@ export const App = () => {
|
|||||||
<LicenseBanner />
|
<LicenseBanner />
|
||||||
<ExternalBanners />
|
<ExternalBanners />
|
||||||
<InternalBanners />
|
<InternalBanners />
|
||||||
|
<EdgeUpgradeBanner />
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<ToastRenderer />
|
<ToastRenderer />
|
||||||
<Routes>
|
<Routes>
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import { useUiFlag } from '../../../hooks/useUiFlag';
|
||||||
|
import { ConditionallyRender } from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Banner } from '../Banner/Banner';
|
||||||
|
import { IBanner } from '../../../interfaces/banner';
|
||||||
|
|
||||||
|
export const EdgeUpgradeBanner = () => {
|
||||||
|
const displayUpgradeEdgeBanner = useUiFlag('displayUpgradeEdgeBanner');
|
||||||
|
const upgradeEdgeBanner: IBanner = {
|
||||||
|
message: `We noticed that you're using an outdated Unleash Edge. To ensure you continue to receive metrics, we recommend upgrading to v17.0.0 or later.`,
|
||||||
|
link: 'https://github.com/Unleash/unleash-edge',
|
||||||
|
linkText: 'Get latest',
|
||||||
|
variant: 'warning',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={displayUpgradeEdgeBanner}
|
||||||
|
show={<Banner key={'upgradeEdge'} banner={upgradeEdgeBanner} />}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -79,6 +79,7 @@ export type UiFlags = {
|
|||||||
executiveDashboard?: boolean;
|
executiveDashboard?: boolean;
|
||||||
changeRequestConflictHandling?: boolean;
|
changeRequestConflictHandling?: boolean;
|
||||||
feedbackComments?: Variant;
|
feedbackComments?: Variant;
|
||||||
|
displayUpgradeEdgeBanner?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
@ -166,6 +166,15 @@ export default class ClientInstanceStore implements IClientInstanceStore {
|
|||||||
return rows.map(mapRow);
|
return rows.map(mapRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getBySdkName(sdkName: string): Promise<IClientInstance[]> {
|
||||||
|
const rows = await this.db
|
||||||
|
.select()
|
||||||
|
.from(TABLE)
|
||||||
|
.whereRaw(`sdk_version LIKE '??%'`, [sdkName])
|
||||||
|
.orderBy('last_seen', 'desc');
|
||||||
|
return rows.map(mapRow);
|
||||||
|
}
|
||||||
|
|
||||||
async getDistinctApplications(): Promise<string[]> {
|
async getDistinctApplications(): Promise<string[]> {
|
||||||
const rows = await this.db
|
const rows = await this.db
|
||||||
.distinct('app_name')
|
.distinct('app_name')
|
||||||
|
@ -19,7 +19,7 @@ import { clientMetricsSchema } from '../shared/schema';
|
|||||||
import { PartialSome } from '../../../types/partial';
|
import { PartialSome } from '../../../types/partial';
|
||||||
import { IPrivateProjectChecker } from '../../private-project/privateProjectCheckerType';
|
import { IPrivateProjectChecker } from '../../private-project/privateProjectCheckerType';
|
||||||
import { IFlagResolver, SYSTEM_USER } from '../../../types';
|
import { IFlagResolver, SYSTEM_USER } from '../../../types';
|
||||||
import { ALL_PROJECTS } from '../../../util';
|
import { ALL_PROJECTS, parseStrictSemVer } from '../../../util';
|
||||||
import { Logger } from '../../../logger';
|
import { Logger } from '../../../logger';
|
||||||
|
|
||||||
export default class ClientInstanceService {
|
export default class ClientInstanceService {
|
||||||
@ -224,4 +224,24 @@ export default class ClientInstanceService {
|
|||||||
async removeInstancesOlderThanTwoDays(): Promise<void> {
|
async removeInstancesOlderThanTwoDays(): Promise<void> {
|
||||||
return this.clientInstanceStore.removeInstancesOlderThanTwoDays();
|
return this.clientInstanceStore.removeInstancesOlderThanTwoDays();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async usesSdkOlderThan(
|
||||||
|
sdkName: string,
|
||||||
|
sdkVersion: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const semver = parseStrictSemVer(sdkVersion);
|
||||||
|
const instancesOfSdk =
|
||||||
|
await this.clientInstanceStore.getBySdkName(sdkName);
|
||||||
|
return instancesOfSdk.some((instance) => {
|
||||||
|
if (instance.sdkVersion) {
|
||||||
|
const [_sdkName, sdkVersion] = instance.sdkVersion.split(':');
|
||||||
|
const instanceUsedSemver = parseStrictSemVer(sdkVersion);
|
||||||
|
return (
|
||||||
|
instanceUsedSemver !== null &&
|
||||||
|
semver !== null &&
|
||||||
|
instanceUsedSemver < semver
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
|
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
|
||||||
} from '../../util/segments';
|
} from '../../util/segments';
|
||||||
import TestAgent from 'supertest/lib/agent';
|
import TestAgent from 'supertest/lib/agent';
|
||||||
|
import { IUnleashStores } from '../../types';
|
||||||
|
|
||||||
const uiConfig = {
|
const uiConfig = {
|
||||||
headerBackground: 'red',
|
headerBackground: 'red',
|
||||||
@ -28,17 +29,20 @@ async function getSetup() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
base,
|
base,
|
||||||
|
stores,
|
||||||
request: supertest(app),
|
request: supertest(app),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let request: TestAgent<Test>;
|
let request: TestAgent<Test>;
|
||||||
let base: string;
|
let base: string;
|
||||||
|
let stores: IUnleashStores;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const setup = await getSetup();
|
const setup = await getSetup();
|
||||||
request = setup.request;
|
request = setup.request;
|
||||||
base = setup.base;
|
base = setup.base;
|
||||||
|
stores = setup.stores;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should get ui config', async () => {
|
test('should get ui config', async () => {
|
||||||
@ -52,3 +56,45 @@ test('should get ui config', async () => {
|
|||||||
expect(body.segmentValuesLimit).toEqual(DEFAULT_SEGMENT_VALUES_LIMIT);
|
expect(body.segmentValuesLimit).toEqual(DEFAULT_SEGMENT_VALUES_LIMIT);
|
||||||
expect(body.strategySegmentsLimit).toEqual(DEFAULT_STRATEGY_SEGMENTS_LIMIT);
|
expect(body.strategySegmentsLimit).toEqual(DEFAULT_STRATEGY_SEGMENTS_LIMIT);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('displayUpgradeEdgeBanner', () => {
|
||||||
|
test('ui config should have displayUpgradeEdgeBanner to be set if an instance using edge has been seen', async () => {
|
||||||
|
await stores.clientInstanceStore.insert({
|
||||||
|
appName: 'my-app',
|
||||||
|
instanceId: 'some-instance',
|
||||||
|
sdkVersion: 'unleash-edge:16.0.0',
|
||||||
|
});
|
||||||
|
const { body } = await request
|
||||||
|
.get(`${base}/api/admin/ui-config`)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
expect(body.flags).toBeTruthy();
|
||||||
|
expect(body.flags.displayUpgradeEdgeBanner).toBeTruthy();
|
||||||
|
});
|
||||||
|
test('ui config should not get displayUpgradeEdgeBanner flag if edge >= 17.0.0 has been seen', async () => {
|
||||||
|
await stores.clientInstanceStore.insert({
|
||||||
|
appName: 'my-app',
|
||||||
|
instanceId: 'some-instance',
|
||||||
|
sdkVersion: 'unleash-edge:17.1.0',
|
||||||
|
});
|
||||||
|
const { body } = await request
|
||||||
|
.get(`${base}/api/admin/ui-config`)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
expect(body.flags).toBeTruthy();
|
||||||
|
expect(body.flags.displayUpgradeEdgeBanner).toEqual(false);
|
||||||
|
});
|
||||||
|
test('ui config should not get displayUpgradeEdgeBanner flag if java-client has been seen', async () => {
|
||||||
|
await stores.clientInstanceStore.insert({
|
||||||
|
appName: 'my-app',
|
||||||
|
instanceId: 'some-instance',
|
||||||
|
sdkVersion: 'unleash-client-java:9.1.0',
|
||||||
|
});
|
||||||
|
const { body } = await request
|
||||||
|
.get(`${base}/api/admin/ui-config`)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
expect(body.flags).toBeTruthy();
|
||||||
|
expect(body.flags.displayUpgradeEdgeBanner).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -26,6 +26,9 @@ import { SetUiConfigSchema } from '../../openapi/spec/set-ui-config-schema';
|
|||||||
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
||||||
import { ProxyService } from '../../services';
|
import { ProxyService } from '../../services';
|
||||||
import MaintenanceService from '../../features/maintenance/maintenance-service';
|
import MaintenanceService from '../../features/maintenance/maintenance-service';
|
||||||
|
import memoizee from 'memoizee';
|
||||||
|
import { minutesToMilliseconds } from 'date-fns';
|
||||||
|
import ClientInstanceService from '../../features/metrics/instance/instance-service';
|
||||||
|
|
||||||
class ConfigController extends Controller {
|
class ConfigController extends Controller {
|
||||||
private versionService: VersionService;
|
private versionService: VersionService;
|
||||||
@ -36,8 +39,12 @@ class ConfigController extends Controller {
|
|||||||
|
|
||||||
private emailService: EmailService;
|
private emailService: EmailService;
|
||||||
|
|
||||||
|
private clientInstanceService: ClientInstanceService;
|
||||||
|
|
||||||
private maintenanceService: MaintenanceService;
|
private maintenanceService: MaintenanceService;
|
||||||
|
|
||||||
|
private usesOldEdgeFunction: () => Promise<boolean>;
|
||||||
|
|
||||||
private readonly openApiService: OpenApiService;
|
private readonly openApiService: OpenApiService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -49,6 +56,7 @@ class ConfigController extends Controller {
|
|||||||
openApiService,
|
openApiService,
|
||||||
proxyService,
|
proxyService,
|
||||||
maintenanceService,
|
maintenanceService,
|
||||||
|
clientInstanceService,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
IUnleashServices,
|
IUnleashServices,
|
||||||
| 'versionService'
|
| 'versionService'
|
||||||
@ -57,6 +65,7 @@ class ConfigController extends Controller {
|
|||||||
| 'openApiService'
|
| 'openApiService'
|
||||||
| 'proxyService'
|
| 'proxyService'
|
||||||
| 'maintenanceService'
|
| 'maintenanceService'
|
||||||
|
| 'clientInstanceService'
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
super(config);
|
super(config);
|
||||||
@ -66,6 +75,18 @@ class ConfigController extends Controller {
|
|||||||
this.openApiService = openApiService;
|
this.openApiService = openApiService;
|
||||||
this.proxyService = proxyService;
|
this.proxyService = proxyService;
|
||||||
this.maintenanceService = maintenanceService;
|
this.maintenanceService = maintenanceService;
|
||||||
|
this.clientInstanceService = clientInstanceService;
|
||||||
|
this.usesOldEdgeFunction = memoizee(
|
||||||
|
async () =>
|
||||||
|
this.clientInstanceService.usesSdkOlderThan(
|
||||||
|
'unleash-edge',
|
||||||
|
'17.0.0',
|
||||||
|
),
|
||||||
|
{
|
||||||
|
promise: true,
|
||||||
|
maxAge: minutesToMilliseconds(10),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@ -109,14 +130,17 @@ class ConfigController extends Controller {
|
|||||||
req: AuthedRequest,
|
req: AuthedRequest,
|
||||||
res: Response<UiConfigSchema>,
|
res: Response<UiConfigSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const [frontendSettings, simpleAuthSettings, maintenanceMode] =
|
const [
|
||||||
await Promise.all([
|
frontendSettings,
|
||||||
this.proxyService.getFrontendSettings(false),
|
simpleAuthSettings,
|
||||||
this.settingService.get<SimpleAuthSettings>(
|
maintenanceMode,
|
||||||
simpleAuthSettingsKey,
|
usesOldEdge,
|
||||||
),
|
] = await Promise.all([
|
||||||
this.maintenanceService.isMaintenanceMode(),
|
this.proxyService.getFrontendSettings(false),
|
||||||
]);
|
this.settingService.get<SimpleAuthSettings>(simpleAuthSettingsKey),
|
||||||
|
this.maintenanceService.isMaintenanceMode(),
|
||||||
|
this.usesOldEdgeFunction(),
|
||||||
|
]);
|
||||||
|
|
||||||
const disablePasswordAuth =
|
const disablePasswordAuth =
|
||||||
simpleAuthSettings?.disabled ||
|
simpleAuthSettings?.disabled ||
|
||||||
@ -126,7 +150,11 @@ class ConfigController extends Controller {
|
|||||||
email: req.user.email,
|
email: req.user.email,
|
||||||
});
|
});
|
||||||
|
|
||||||
const flags = { ...this.config.ui.flags, ...expFlags };
|
const flags = {
|
||||||
|
...this.config.ui.flags,
|
||||||
|
...expFlags,
|
||||||
|
displayUpgradeEdgeBanner: usesOldEdge,
|
||||||
|
};
|
||||||
|
|
||||||
const response: UiConfigSchema = {
|
const response: UiConfigSchema = {
|
||||||
...this.config.ui,
|
...this.config.ui,
|
||||||
|
@ -21,6 +21,7 @@ export interface IClientInstanceStore
|
|||||||
setLastSeen(INewClientInstance): Promise<void>;
|
setLastSeen(INewClientInstance): Promise<void>;
|
||||||
insert(details: INewClientInstance): Promise<void>;
|
insert(details: INewClientInstance): Promise<void>;
|
||||||
getByAppName(appName: string): Promise<IClientInstance[]>;
|
getByAppName(appName: string): Promise<IClientInstance[]>;
|
||||||
|
getBySdkName(sdkName: string): Promise<IClientInstance[]>;
|
||||||
getDistinctApplications(): Promise<string[]>;
|
getDistinctApplications(): Promise<string[]>;
|
||||||
getDistinctApplicationsCount(daysBefore?: number): Promise<number>;
|
getDistinctApplicationsCount(daysBefore?: number): Promise<number>;
|
||||||
deleteForApplication(appName: string): Promise<void>;
|
deleteForApplication(appName: string): Promise<void>;
|
||||||
|
@ -31,6 +31,14 @@ export default class FakeClientInstanceStore implements IClientInstanceStore {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getBySdkName(sdkName: string): Promise<IClientInstance[]> {
|
||||||
|
return Promise.resolve(
|
||||||
|
this.instances.filter((instance) =>
|
||||||
|
instance.sdkVersion?.startsWith(sdkName),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async deleteAll(): Promise<void> {
|
async deleteAll(): Promise<void> {
|
||||||
this.instances = [];
|
this.instances = [];
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user