Skip to content

Commit

Permalink
Merge pull request #130439 from dhartunian/backport23.1-120475-120490…
Browse files Browse the repository at this point in the history
…-129420

release-23.1: ui: add license change notification to db console
  • Loading branch information
dhartunian committed Sep 12, 2024
2 parents 9f7032c + afac9c9 commit ed8069c
Show file tree
Hide file tree
Showing 14 changed files with 262 additions and 20 deletions.
3 changes: 2 additions & 1 deletion pkg/server/server_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ func (s *httpServer) setupRoutes(
}
return nil
},
Flags: flags,
Flags: flags,
Settings: s.cfg.Settings,
})

// The authentication mux used here is created in "allow anonymous" mode so that the UI
Expand Down
6 changes: 3 additions & 3 deletions pkg/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ Binary built without web UI.
respBytes, err = io.ReadAll(resp.Body)
require.NoError(t, err)
expected := fmt.Sprintf(
`{"Insecure":true,"LoggedInUser":null,"Tag":"%s","Version":"%s","NodeID":"%d","OIDCAutoLogin":false,"OIDCLoginEnabled":false,"OIDCButtonText":"","FeatureFlags":{"can_view_kv_metric_dashboards":true},"OIDCGenerateJWTAuthTokenEnabled":false}`,
`{"Insecure":true,"LoggedInUser":null,"Tag":"%s","Version":"%s","NodeID":"%d","OIDCAutoLogin":false,"OIDCLoginEnabled":false,"OIDCButtonText":"","FeatureFlags":{"can_view_kv_metric_dashboards":true},"OIDCGenerateJWTAuthTokenEnabled":false,"LicenseType":"OSS","SecondsUntilLicenseExpiry":0}`,
build.GetInfo().Tag,
build.BinaryVersionPrefix(),
1,
Expand Down Expand Up @@ -831,7 +831,7 @@ Binary built without web UI.
{
loggedInClient,
fmt.Sprintf(
`{"Insecure":false,"LoggedInUser":"authentic_user","Tag":"%s","Version":"%s","NodeID":"%d","OIDCAutoLogin":false,"OIDCLoginEnabled":false,"OIDCButtonText":"","FeatureFlags":{"can_view_kv_metric_dashboards":true},"OIDCGenerateJWTAuthTokenEnabled":false}`,
`{"Insecure":false,"LoggedInUser":"authentic_user","Tag":"%s","Version":"%s","NodeID":"%d","OIDCAutoLogin":false,"OIDCLoginEnabled":false,"OIDCButtonText":"","FeatureFlags":{"can_view_kv_metric_dashboards":true},"OIDCGenerateJWTAuthTokenEnabled":false,"LicenseType":"OSS","SecondsUntilLicenseExpiry":0}`,
build.GetInfo().Tag,
build.BinaryVersionPrefix(),
1,
Expand All @@ -840,7 +840,7 @@ Binary built without web UI.
{
loggedOutClient,
fmt.Sprintf(
`{"Insecure":false,"LoggedInUser":null,"Tag":"%s","Version":"%s","NodeID":"%d","OIDCAutoLogin":false,"OIDCLoginEnabled":false,"OIDCButtonText":"","FeatureFlags":{"can_view_kv_metric_dashboards":true},"OIDCGenerateJWTAuthTokenEnabled":false}`,
`{"Insecure":false,"LoggedInUser":null,"Tag":"%s","Version":"%s","NodeID":"%d","OIDCAutoLogin":false,"OIDCLoginEnabled":false,"OIDCButtonText":"","FeatureFlags":{"can_view_kv_metric_dashboards":true},"OIDCGenerateJWTAuthTokenEnabled":false,"LicenseType":"OSS","SecondsUntilLicenseExpiry":0}`,
build.GetInfo().Tag,
build.BinaryVersionPrefix(),
1,
Expand Down
1 change: 1 addition & 0 deletions pkg/ui/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ go_library(
"//pkg/build",
"//pkg/server/serverpb",
"//pkg/settings",
"//pkg/settings/cluster",
"//pkg/util/httputil",
"//pkg/util/log",
],
Expand Down
13 changes: 13 additions & 0 deletions pkg/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/cockroachdb/cockroach/pkg/build"
"github.com/cockroachdb/cockroach/pkg/server/serverpb"
"github.com/cockroachdb/cockroach/pkg/settings"
"github.com/cockroachdb/cockroach/pkg/settings/cluster"
"github.com/cockroachdb/cockroach/pkg/util/httputil"
"github.com/cockroachdb/cockroach/pkg/util/log"
)
Expand Down Expand Up @@ -92,6 +93,9 @@ type indexHTMLArgs struct {
FeatureFlags serverpb.FeatureFlags

OIDCGenerateJWTAuthTokenEnabled bool

LicenseType string
SecondsUntilLicenseExpiry int64
}

// OIDCUIConf is a variable that stores data required by the
Expand Down Expand Up @@ -129,6 +133,7 @@ type Config struct {
GetUser func(ctx context.Context) *string
OIDC OIDCUI
Flags serverpb.FeatureFlags
Settings *cluster.Settings
}

var uiConfigPath = regexp.MustCompile("^/uiconfig$")
Expand Down Expand Up @@ -158,6 +163,11 @@ func Handler(cfg Config) http.Handler {
buildInfo := build.GetInfo()

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
licenseType, err := base.LicenseType(cfg.Settings)
if err != nil {
log.Errorf(context.Background(), "unable to get license type: %+v", err)
}
licenseTTL := base.LicenseTTL.Value()
oidcConf := cfg.OIDC.GetOIDCConf()
args := indexHTMLArgs{
Insecure: cfg.Insecure,
Expand All @@ -170,6 +180,9 @@ func Handler(cfg Config) http.Handler {
FeatureFlags: cfg.Flags,

OIDCGenerateJWTAuthTokenEnabled: oidcConf.GenerateJWTAuthTokenEnabled,

LicenseType: licenseType,
SecondsUntilLicenseExpiry: licenseTTL,
}
if cfg.NodeID != nil {
args.NodeID = cfg.NodeID.String()
Expand Down
2 changes: 2 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/util/dataFromServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export interface DataFromServer {
OIDCButtonText: string;
OIDCGenerateJWTAuthTokenEnabled: boolean;
FeatureFlags: FeatureFlags;
LicenseType: string;
SecondsUntilLicenseExpiry: number;
}

// Tell TypeScript about `window.dataFromServer`, which is set in a script
Expand Down
29 changes: 29 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 @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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: [],
Expand Down
190 changes: 176 additions & 14 deletions pkg/ui/workspaces/db-console/src/redux/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -55,6 +56,7 @@ export enum AlertLevel {
WARNING,
CRITICAL,
SUCCESS,
INFORMATION,
}

export interface AlertInfo {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -684,6 +672,178 @@ export const dataFromServerAlertSelector = createSelector(
},
);

const licenseTypeNames = new Map<
string,
"Trial" | "Enterprise" | "Non-Commercial" | "None"
>([
["Evaluation", "Trial"],
["Enterprise", "Enterprise"],
["NonCommercial", "Non-Commercial"],
["OSS", "None"],
["BSD", "None"],
]);

// licenseTypeSelector returns user-friendly names of license types.
export const licenseTypeSelector = createSelector(
getDataFromServer,
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,
data => {
return Math.ceil(data.SecondsUntilLicenseExpiry / 86400); // seconds in 1 day
},
);

export const showLicenseTTLLocalSetting = new LocalSetting(
"show_license_ttl",
localSettingsSelector,
{ show: true },
);

export const showLicenseTTLAlertSelector = createSelector(
showLicenseTTLLocalSetting.selector,
daysUntilLicenseExpiresSelector,
licenseTypeSelector,
(showLicenseTTL, daysUntilLicenseExpired, licenseType): Alert => {
if (!showLicenseTTL.show) {
return;
}
if (licenseType === "None") {
return;
}
const daysToShowAlert = 14;
let title: string;
let level: AlertLevel;

if (daysUntilLicenseExpired > daysToShowAlert) {
return;
} else if (daysUntilLicenseExpired < 0) {
title = `License expired ${Math.abs(daysUntilLicenseExpired)} days ago`;
level = AlertLevel.CRITICAL;
} else if (daysUntilLicenseExpired === 0) {
title = `License expired`;
level = AlertLevel.CRITICAL;
} else if (daysUntilLicenseExpired <= daysToShowAlert) {
title = `License expires in ${daysUntilLicenseExpired} days`;
level = AlertLevel.WARNING;
}
return {
level: level,
title: title,
showAsAlert: true,
autoClose: false,
closable: true,
dismiss: (dispatch: Dispatch<Action>) => {
dispatch(showLicenseTTLLocalSetting.set({ show: false }));
return Promise.resolve();
},
};
},
);

/**
* Selector which returns an array of all active alerts which should be
* displayed as a banner, which appears at the top of the page and overlaps
Expand All @@ -698,6 +858,7 @@ export const bannerAlertsSelector = createSelector(
terminateSessionAlertSelector,
terminateQueryAlertSelector,
dataFromServerAlertSelector,
showLicenseTTLAlertSelector,
(...alerts: Alert[]): Alert[] => {
return _.without(alerts, null, undefined);
},
Expand Down Expand Up @@ -744,6 +905,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
Loading

0 comments on commit ed8069c

Please sign in to comment.