From fe0aa3a5771aedc626cb04c9706071f080f13685 Mon Sep 17 00:00:00 2001 From: David Hartunian Date: Mon, 19 Aug 2024 17:20:19 -0400 Subject: [PATCH] ui: add license change notification to db console This change adds a dismissable alert to the Overview page of DB Console that informs users about upcoming license changes. This popup is only shown if the cluster does not have an active "Enterprise" license The popup links to this page: "https://www.cockroachlabs.com/enterprise-license-update/" When the popup is dismissed, the dismissal is stored in the DB for this user and they don't see this notification again. Resolves: CRDB-40939 Release note (ui change): DB Console will show a notification alerting customers without an Enterprise license, to upcoming license changes with a link to more information. --- .../db-console/src/redux/alerts.spec.ts | 29 +++++ .../workspaces/db-console/src/redux/alerts.ts | 117 +++++++++++++++--- .../workspaces/db-console/src/redux/uiData.ts | 5 + pkg/ui/workspaces/db-console/src/util/docs.ts | 2 + .../shared/components/alertBox/alertbox.styl | 7 ++ .../shared/components/alertBox/index.tsx | 5 +- .../views/shared/components/icons/index.tsx | 13 ++ 7 files changed, 163 insertions(+), 15 deletions(-) diff --git a/pkg/ui/workspaces/db-console/src/redux/alerts.spec.ts b/pkg/ui/workspaces/db-console/src/redux/alerts.spec.ts index ed8ec55b89b3..14f20ef8f551 100644 --- a/pkg/ui/workspaces/db-console/src/redux/alerts.spec.ts +++ b/pkg/ui/workspaces/db-console/src/redux/alerts.spec.ts @@ -15,6 +15,7 @@ import { createHashHistory } from "history"; import * as protos from "src/js/protos"; import { cockroach } from "src/js/protos"; import { API_PREFIX } from "src/util/api"; +import { setDataFromServer } from "src/util/dataFromServer"; import fetchMock from "src/util/fetch-mock"; import { AdminUIState, AppDispatch, createAdminUIStore } from "./state"; @@ -31,11 +32,13 @@ import { emailSubscriptionAlertSelector, clusterPreserveDowngradeOptionDismissedSetting, clusterPreserveDowngradeOptionOvertimeSelector, + licenseUpdateNotificationSelector, } from "./alerts"; import { versionsSelector } from "src/redux/nodes"; import { VERSION_DISMISSED_KEY, INSTRUCTIONS_BOX_COLLAPSED_KEY, + LICENSE_UPDATE_DISMISSED_KEY, setUIDataKey, isInFlight, } from "./uiData"; @@ -257,6 +260,31 @@ describe("alerts", function () { }); }); + describe("licence update notification", function () { + it("displays the alert when nothing is done", function () { + dispatch(setUIDataKey(LICENSE_UPDATE_DISMISSED_KEY, null)); + const alert = licenseUpdateNotificationSelector(state()); + expect(typeof alert).toBe("object"); + expect(alert.level).toEqual(AlertLevel.INFORMATION); + expect(alert.text).toEqual( + "Important changes to CockroachDB’s licensing model.", + ); + }); + + it("hides the alert when dismissed timestamp is present", function () { + dispatch(setUIDataKey(LICENSE_UPDATE_DISMISSED_KEY, moment())); + expect(licenseUpdateNotificationSelector(state())).toBeUndefined(); + }); + + it("hides the alert when license is enterprise", function () { + dispatch(setUIDataKey(LICENSE_UPDATE_DISMISSED_KEY, null)); + setDataFromServer({ + LicenseType: "Enterprise", + } as any); + expect(licenseUpdateNotificationSelector(state())).toBeUndefined(); + }); + }); + describe("new version available notification", function () { it("displays nothing when versions have not yet been loaded", function () { dispatch(setUIDataKey(VERSION_DISMISSED_KEY, null)); @@ -630,6 +658,7 @@ describe("alerts", function () { ); dispatch(setUIDataKey(VERSION_DISMISSED_KEY, "blank")); dispatch(setUIDataKey(INSTRUCTIONS_BOX_COLLAPSED_KEY, false)); + dispatch(setUIDataKey(LICENSE_UPDATE_DISMISSED_KEY, moment())); dispatch( versionReducerObj.receiveData({ details: [], diff --git a/pkg/ui/workspaces/db-console/src/redux/alerts.ts b/pkg/ui/workspaces/db-console/src/redux/alerts.ts index 6a7b1a134c39..a798c4ff4612 100644 --- a/pkg/ui/workspaces/db-console/src/redux/alerts.ts +++ b/pkg/ui/workspaces/db-console/src/redux/alerts.ts @@ -21,6 +21,7 @@ import { ThunkAction } from "redux-thunk"; import { LocalSetting } from "./localsettings"; import { + LICENSE_UPDATE_DISMISSED_KEY, VERSION_DISMISSED_KEY, INSTRUCTIONS_BOX_COLLAPSED_KEY, saveUIData, @@ -55,6 +56,7 @@ export enum AlertLevel { WARNING, CRITICAL, SUCCESS, + INFORMATION, } export interface AlertInfo { @@ -629,20 +631,6 @@ export const upgradeNotFinalizedWarningSelector = createSelector( }, ); -/** - * Selector which returns an array of all active alerts which should be - * displayed in the overview list page, these should be non-critical alerts. - */ - -export const overviewListAlertsSelector = createSelector( - staggeredVersionWarningSelector, - clusterPreserveDowngradeOptionOvertimeSelector, - upgradeNotFinalizedWarningSelector, - (...alerts: Alert[]): Alert[] => { - return _.without(alerts, null, undefined); - }, -); - /** * Selector which returns an array of all active alerts which should be * displayed in the alerts panel, which is embedded within the cluster overview @@ -701,6 +689,106 @@ export const licenseTypeSelector = createSelector( data => licenseTypeNames.get(data.LicenseType) || "None", ); +export const licenseUpdateDismissedLocalSetting = new LocalSetting( + "license_update_dismissed", + localSettingsSelector, + moment(0), +); + +const licenseUpdateDismissedPersistentLoadedSelector = createSelector( + (state: AdminUIState) => state.uiData, + uiData => + uiData && + Object.prototype.hasOwnProperty.call(uiData, LICENSE_UPDATE_DISMISSED_KEY), +); + +const licenseUpdateDismissedPersistentSelector = createSelector( + (state: AdminUIState) => state.uiData, + uiData => moment(uiData?.[LICENSE_UPDATE_DISMISSED_KEY]?.data ?? 0), +); + +export const licenseUpdateNotificationSelector = createSelector( + licenseTypeSelector, + licenseUpdateDismissedLocalSetting.selector, + licenseUpdateDismissedPersistentSelector, + licenseUpdateDismissedPersistentLoadedSelector, + ( + licenseType, + licenseUpdateDismissed, + licenseUpdateDismissedPersistent, + licenseUpdateDismissedPersistentLoaded, + ): Alert => { + // If customer has Enterprise license they don't need to worry about this. + if (licenseType === "Enterprise") { + return undefined; + } + + // If the notification has been dismissed based on the session storage + // timestamp, don't show it.' + // + // Note: `licenseUpdateDismissed` is wrapped in `moment()` because + // the local storage selector won't convert it back from a string. + // We omit fixing that here since this change is being backported + // to many versions. + if (moment(licenseUpdateDismissed).isAfter(moment(0))) { + return undefined; + } + + // If the notification has been dismissed based on the uiData + // storage in the cluster, don't show it. Note that this is + // different from how version upgrade notifications work, this one + // is dismissed forever and won't return even if you upgrade + // further or time passes. + if ( + licenseUpdateDismissedPersistentLoaded && + licenseUpdateDismissedPersistent && + licenseUpdateDismissedPersistent.isAfter(moment(0)) + ) { + return undefined; + } + + return { + level: AlertLevel.INFORMATION, + title: "Coming November 18, 2024", + text: "Important changes to CockroachDB’s licensing model.", + link: docsURL.enterpriseLicenseUpdate, + dismiss: (dispatch: any) => { + const dismissedAt = moment(); + // Note(davidh): I haven't been able to find historical context + // for why some alerts have both a "local" and a "persistent" + // dismissal. My thinking is that just the persistent dismissal + // should be adequate, but I'm preserving that behavior here to + // match the version upgrade notification. + + // Dismiss locally. + dispatch(licenseUpdateDismissedLocalSetting.set(dismissedAt)); + // Dismiss persistently. + return dispatch( + saveUIData({ + key: LICENSE_UPDATE_DISMISSED_KEY, + value: dismissedAt.valueOf(), + }), + ); + }, + }; + }, +); + +/** + * Selector which returns an array of all active alerts which should be + * displayed in the overview list page, these should be non-critical alerts. + */ + +export const overviewListAlertsSelector = createSelector( + staggeredVersionWarningSelector, + clusterPreserveDowngradeOptionOvertimeSelector, + upgradeNotFinalizedWarningSelector, + licenseUpdateNotificationSelector, + (...alerts: Alert[]): Alert[] => { + return _.without(alerts, null, undefined); + }, +); + // daysUntilLicenseExpiresSelector returns number of days remaining before license expires. export const daysUntilLicenseExpiresSelector = createSelector( getDataFromServer, @@ -774,6 +862,7 @@ export function alertDataSync(store: Store) { const keysToMaybeLoad = [ VERSION_DISMISSED_KEY, INSTRUCTIONS_BOX_COLLAPSED_KEY, + LICENSE_UPDATE_DISMISSED_KEY, ]; const keysToLoad = _.filter(keysToMaybeLoad, key => { return !(_.has(uiData, key) || isInFlight(state, key)); diff --git a/pkg/ui/workspaces/db-console/src/redux/uiData.ts b/pkg/ui/workspaces/db-console/src/redux/uiData.ts index afda71bb7c93..0eb3fc1c4300 100644 --- a/pkg/ui/workspaces/db-console/src/redux/uiData.ts +++ b/pkg/ui/workspaces/db-console/src/redux/uiData.ts @@ -56,6 +56,11 @@ export class OptInAttributes { // was last dismissed. export const VERSION_DISMISSED_KEY = "version_dismissed"; +// LICENSE_UPDATE_DISMISSED_KEY is the uiData key on the server that tracks when the licence +// update banner was last dismissed. This banner notifies the user that we've changed our +// licensing if they're deployed without an active license. +export const LICENSE_UPDATE_DISMISSED_KEY = "license_update_dismissed"; + // INSTRUCTIONS_BOX_COLLAPSED_KEY is the uiData key on the server that tracks whether the // instructions box on the cluster viz has been collapsed or not. export const INSTRUCTIONS_BOX_COLLAPSED_KEY = diff --git a/pkg/ui/workspaces/db-console/src/util/docs.ts b/pkg/ui/workspaces/db-console/src/util/docs.ts index f1ace7749197..8bfa99a9530a 100644 --- a/pkg/ui/workspaces/db-console/src/util/docs.ts +++ b/pkg/ui/workspaces/db-console/src/util/docs.ts @@ -63,6 +63,8 @@ export let upgradeTroubleshooting: string; export let licensingFaqs: string; // Note that these explicitly don't use the current version, since we want to // link to the most up-to-date documentation available. +export const enterpriseLicenseUpdate = + "https://www.cockroachlabs.com/enterprise-license-update/"; export const upgradeCockroachVersion = "https://www.cockroachlabs.com/docs/stable/upgrade-cockroach-version.html"; export const enterpriseLicensing = diff --git a/pkg/ui/workspaces/db-console/src/views/shared/components/alertBox/alertbox.styl b/pkg/ui/workspaces/db-console/src/views/shared/components/alertBox/alertbox.styl index 3b80a4e2bd34..d0773a910572 100644 --- a/pkg/ui/workspaces/db-console/src/views/shared/components/alertBox/alertbox.styl +++ b/pkg/ui/workspaces/db-console/src/views/shared/components/alertBox/alertbox.styl @@ -46,6 +46,13 @@ border-color $notification-info-border-color background-color $notification-info-fill-color + &--information + color $body-color + border-color $notification-info-border-color + background-color $notification-info-fill-color + path + fill $colors--primary-blue-3 + &--warning color $body-color border-color $notification-warning-border-color diff --git a/pkg/ui/workspaces/db-console/src/views/shared/components/alertBox/index.tsx b/pkg/ui/workspaces/db-console/src/views/shared/components/alertBox/index.tsx index b1e7e600cf5e..5478349ca7cd 100644 --- a/pkg/ui/workspaces/db-console/src/views/shared/components/alertBox/index.tsx +++ b/pkg/ui/workspaces/db-console/src/views/shared/components/alertBox/index.tsx @@ -18,6 +18,7 @@ import { warningIcon, notificationIcon, criticalIcon, + informationIcon, } from "src/views/shared/components/icons"; import { trustIcon } from "src/util/trust"; @@ -27,6 +28,8 @@ function alertIcon(level: AlertLevel) { return trustIcon(criticalIcon); case AlertLevel.WARNING: return trustIcon(warningIcon); + case AlertLevel.INFORMATION: + return trustIcon(informationIcon); default: return trustIcon(notificationIcon); } @@ -49,7 +52,7 @@ export class AlertBox extends React.Component { const learnMore = this.props.link && ( - Learn More. + Learn More ); content = ( diff --git a/pkg/ui/workspaces/db-console/src/views/shared/components/icons/index.tsx b/pkg/ui/workspaces/db-console/src/views/shared/components/icons/index.tsx index 35bb1ec73f31..da6efc626cf9 100644 --- a/pkg/ui/workspaces/db-console/src/views/shared/components/icons/index.tsx +++ b/pkg/ui/workspaces/db-console/src/views/shared/components/icons/index.tsx @@ -98,6 +98,19 @@ export const warningIcon: string = ` + + + + + + + + + +`; + export const notificationIcon: string = `