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..ecd45bc5f4c2 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"; @@ -47,6 +50,7 @@ import { healthReducerObj, settingsReducerObj, } from "./apiReducers"; +import { loginSuccess } from "./login"; import Long from "long"; import MembershipStatus = cockroach.kv.kvserver.liveness.livenesspb.MembershipStatus; import { loginSuccess } from "./login"; @@ -257,6 +261,29 @@ 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 +657,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..9d9959514cc0 100644 --- a/pkg/ui/workspaces/db-console/src/redux/alerts.ts +++ b/pkg/ui/workspaces/db-console/src/redux/alerts.ts @@ -48,6 +48,18 @@ import { selectClusterSettings, selectClusterSettingVersion, } from "./clusterSettings"; +import { LocalSetting } from "./localsettings"; +import { AdminUIState, AppDispatch } from "./state"; +import { + LICENSE_UPDATE_DISMISSED_KEY, + VERSION_DISMISSED_KEY, + INSTRUCTIONS_BOX_COLLAPSED_KEY, + saveUIData, + loadUIData, + isInFlight, + UIDataState, + UIDataStatus, +} from "./uiData"; import { longToInt } from "src/util/fixLong"; export enum AlertLevel { @@ -55,6 +67,7 @@ export enum AlertLevel { WARNING, CRITICAL, SUCCESS, + INFORMATION, } export interface AlertInfo { @@ -701,6 +714,104 @@ 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 && uiData.hasOwnProperty(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 +885,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..c9df508f7c27 100644 --- a/pkg/ui/workspaces/db-console/src/util/docs.ts +++ b/pkg/ui/workspaces/db-console/src/util/docs.ts @@ -63,6 +63,7 @@ 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..6e992f7f28a0 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 = `