Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

release-23.1: ui: add license change notification to db console #130439

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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