Skip to content

Commit

Permalink
ui: add license change notification to db console
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dhartunian committed Sep 9, 2024
1 parent 05ee4dc commit a597c17
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 15 deletions.
27 changes: 27 additions & 0 deletions pkg/ui/workspaces/db-console/src/redux/alerts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as protos from "src/js/protos";
import { cockroach } from "src/js/protos";
import { versionsSelector } from "src/redux/nodes";
import { API_PREFIX } from "src/util/api";
import { setDataFromServer } from "src/util/dataFromServer";
import fetchMock from "src/util/fetch-mock";

import {
Expand All @@ -32,6 +33,7 @@ import {
emailSubscriptionAlertSelector,
clusterPreserveDowngradeOptionDismissedSetting,
clusterPreserveDowngradeOptionOvertimeSelector,
licenseUpdateNotificationSelector,
} from "./alerts";
import {
livenessReducerObj,
Expand All @@ -46,6 +48,7 @@ import { AdminUIState, AppDispatch, createAdminUIStore } from "./state";
import {
VERSION_DISMISSED_KEY,
INSTRUCTIONS_BOX_COLLAPSED_KEY,
LICENSE_UPDATE_DISMISSED_KEY,
setUIDataKey,
isInFlight,
} from "./uiData";
Expand Down Expand Up @@ -258,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));
Expand Down Expand Up @@ -631,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: [],
Expand Down
115 changes: 101 additions & 14 deletions pkg/ui/workspaces/db-console/src/redux/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
import { LocalSetting } from "./localsettings";
import { AdminUIState, AppDispatch } from "./state";
import {
LICENSE_UPDATE_DISMISSED_KEY,
VERSION_DISMISSED_KEY,
INSTRUCTIONS_BOX_COLLAPSED_KEY,
saveUIData,
Expand All @@ -61,6 +62,7 @@ export enum AlertLevel {
WARNING,
CRITICAL,
SUCCESS,
INFORMATION,
}

export interface AlertInfo {
Expand Down Expand Up @@ -635,20 +637,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
Expand Down Expand Up @@ -707,6 +695,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,
Expand Down Expand Up @@ -780,6 +866,7 @@ export function alertDataSync(store: Store<AdminUIState>) {
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));
Expand Down
5 changes: 5 additions & 0 deletions pkg/ui/workspaces/db-console/src/redux/uiData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,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 =
Expand Down
1 change: 1 addition & 0 deletions pkg/ui/workspaces/db-console/src/util/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
warningIcon,
notificationIcon,
criticalIcon,
informationIcon,
} from "src/views/shared/components/icons";

import "./alertbox.styl";
Expand All @@ -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);
}
Expand All @@ -49,7 +52,7 @@ export class AlertBox extends React.Component<AlertBoxProps, {}> {

const learnMore = this.props.link && (
<a className="" href={this.props.link}>
Learn More.
Learn More
</a>
);
content = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ export const warningIcon = `<svg width="21px" height="21px" viewBox="0 0 21 21"
</g>
</svg>`;

export const informationIcon = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_11029_22094)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 12C0 5.37261 5.39028 0 12 0C18.6274 0 24 5.37261 24 12C24 18.6274 18.6274 24 12 24C5.37261 24 0 18.6274 0 12ZM11.1163 18.7865H12.8836V9.52586H11.1163V18.7865ZM11.1163 7.68786H12.8836V5.21364H11.1163V7.68786Z" fill="#394455"/>
</g>
<defs>
<clipPath id="clip0_11029_22094">
<rect width="24" height="24" fill="white" transform="matrix(1 0 0 -1 0 24)"/>
</clipPath>
</defs>
</svg>
`

export const notificationIcon = `
<svg width="22px" height="22px" viewBox="0 0 38 38" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.3.3 (12081) - http://www.bohemiancoding.com/sketch -->
Expand Down

0 comments on commit a597c17

Please sign in to comment.