feat(ui): hide the "Upgrade Now" button for Cloud users when appropriate

pull/17989/head
Timmy Luong 2020-05-05 18:21:27 -07:00
parent 7f8aa4c40f
commit 4e0672e78e
10 changed files with 330 additions and 123 deletions

View File

@ -0,0 +1,48 @@
// API
import {getOrgSettings as getOrgSettingsAJAX} from 'src/cloud/apis/orgsettings'
// Constants
import {FREE_ORG_HIDE_UPGRADE_SETTING} from 'src/cloud/constants'
// Types
import {GetState, OrgSetting} from 'src/types'
// Selectors
import {getOrg} from 'src/organizations/selectors'
export enum ActionTypes {
SetOrgSettings = 'SET_ORG_SETTINGS',
}
export type Actions = SetOrgSettings
export interface SetOrgSettings {
type: ActionTypes.SetOrgSettings
payload: {orgSettings: OrgSetting[]}
}
export const setOrgSettings = (settings: OrgSetting[]): SetOrgSettings => {
return {
type: ActionTypes.SetOrgSettings,
payload: {orgSettings: settings},
}
}
export const setFreeOrgSettings = (): SetOrgSettings => {
return {
type: ActionTypes.SetOrgSettings,
payload: {orgSettings: [FREE_ORG_HIDE_UPGRADE_SETTING]},
}
}
export const getOrgSettings = () => async (dispatch, getState: GetState) => {
try {
const org = getOrg(getState())
const result = await getOrgSettingsAJAX(org.id)
dispatch(setOrgSettings(result.settings))
} catch (error) {
dispatch(setFreeOrgSettings())
console.error(error)
}
}

View File

@ -0,0 +1,16 @@
import AJAX from 'src/utils/ajax'
import {OrgSettings} from 'src/types'
export const getOrgSettings = async (orgID: string): Promise<OrgSettings> => {
try {
const {data} = await AJAX({
method: 'GET',
url: `/api/v2private/orgs/${orgID}/settings`,
})
return data
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -0,0 +1,34 @@
// Libraries
import {PureComponent} from 'react'
import {connect} from 'react-redux'
// Constants
import {CLOUD} from 'src/shared/constants'
// Actions
import {getOrgSettings as getOrgSettingsAction} from 'src/cloud/actions/orgsettings'
interface DispatchProps {
getOrgSettings: typeof getOrgSettingsAction
}
class OrgSettings extends PureComponent<DispatchProps> {
public componentDidMount() {
if (CLOUD) {
this.props.getOrgSettings()
}
}
public render() {
return this.props.children
}
}
const mdtp: DispatchProps = {
getOrgSettings: getOrgSettingsAction,
}
export default connect<{}, DispatchProps, {}>(
null,
mdtp
)(OrgSettings)

View File

@ -25,3 +25,13 @@ export const DemoDataTemplates = {
DemoDataDashboards[WebsiteMonitoringBucket]
),
}
export const HIDE_UPGRADE_CTA_KEY = 'hide_upgrade_cta'
export const FREE_ORG_HIDE_UPGRADE_SETTING = {
key: HIDE_UPGRADE_CTA_KEY,
value: 'false',
}
export const PAID_ORG_HIDE_UPGRADE_SETTING = {
key: HIDE_UPGRADE_CTA_KEY,
value: 'true',
}

View File

@ -0,0 +1,26 @@
import {produce} from 'immer'
import {Actions, ActionTypes} from 'src/cloud/actions/orgsettings'
import {OrgSetting} from 'src/types'
import {PAID_ORG_HIDE_UPGRADE_SETTING} from 'src/cloud/constants'
export interface OrgSettingsState {
settings: OrgSetting[]
}
export const defaultState: OrgSettingsState = {
settings: [PAID_ORG_HIDE_UPGRADE_SETTING],
}
export const orgSettingsReducer = (
state = defaultState,
action: Actions
): OrgSettingsState =>
produce(state, draftState => {
if (action.type === ActionTypes.SetOrgSettings) {
const {orgSettings} = action.payload
draftState.settings = orgSettings
}
return
})

View File

@ -11,16 +11,21 @@ import NavHeader from 'src/pageLayout/components/NavHeader'
import CloudUpgradeNavBanner from 'src/shared/components/CloudUpgradeNavBanner'
import CloudExclude from 'src/shared/components/cloud/CloudExclude'
import CloudOnly from 'src/shared/components/cloud/CloudOnly'
import OrgSettings from 'src/cloud/components/OrgSettings'
import {FeatureFlag} from 'src/shared/utils/featureFlag'
// Constants
import {generateNavItems} from 'src/pageLayout/constants/navigationHierarchy'
import {
HIDE_UPGRADE_CTA_KEY,
PAID_ORG_HIDE_UPGRADE_SETTING,
} from 'src/cloud/constants'
// Utils
import {getNavItemActivation} from 'src/pageLayout/utils'
// Types
import {AppState, NavBarState} from 'src/types'
import {AppState, NavBarState, OrgSetting} from 'src/types'
// Actions
import {setNavBarState} from 'src/shared/actions/app'
@ -31,6 +36,7 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
interface StateProps {
isHidden: boolean
navBarState: NavBarState
showUpgradeButton: boolean
}
interface DispatchProps {
@ -47,6 +53,7 @@ class TreeSidebar extends PureComponent<Props> {
params: {orgID},
navBarState,
handleSetNavBarState,
showUpgradeButton,
} = this.props
if (isHidden) {
@ -67,132 +74,134 @@ class TreeSidebar extends PureComponent<Props> {
const navItems = generateNavItems(orgID)
return (
<TreeNav
expanded={isExpanded}
headerElement={<NavHeader link={orgPrefix} />}
userElement={<UserWidget />}
onToggleClick={handleToggleNavExpansion}
bannerElement={<CloudUpgradeNavBanner />}
>
{navItems.map(item => {
const linkElement = (className: string): JSX.Element => {
if (item.link.type === 'href') {
return <a href={item.link.location} className={className} />
}
<OrgSettings>
<TreeNav
expanded={isExpanded}
headerElement={<NavHeader link={orgPrefix} />}
userElement={<UserWidget />}
onToggleClick={handleToggleNavExpansion}
bannerElement={showUpgradeButton ? <CloudUpgradeNavBanner /> : null}
>
{navItems.map(item => {
const linkElement = (className: string): JSX.Element => {
if (item.link.type === 'href') {
return <a href={item.link.location} className={className} />
}
return <Link to={item.link.location} className={className} />
}
let navItemElement = (
<TreeNav.Item
key={item.id}
id={item.id}
testID={item.testID}
icon={<Icon glyph={item.icon} />}
label={item.label}
shortLabel={item.shortLabel}
active={getNavItemActivation(
item.activeKeywords,
location.pathname
)}
linkElement={linkElement}
>
{Boolean(item.menu) && (
<TreeNav.SubMenu>
{item.menu.map(menuItem => {
const linkElement = (className: string): JSX.Element => {
if (menuItem.link.type === 'href') {
return (
<a
href={menuItem.link.location}
className={className}
/>
)
}
return <Link to={item.link.location} className={className} />
}
let navItemElement = (
<TreeNav.Item
key={item.id}
id={item.id}
testID={item.testID}
icon={<Icon glyph={item.icon} />}
label={item.label}
shortLabel={item.shortLabel}
active={getNavItemActivation(
item.activeKeywords,
location.pathname
)}
linkElement={linkElement}
>
{Boolean(item.menu) && (
<TreeNav.SubMenu>
{item.menu.map(menuItem => {
const linkElement = (className: string): JSX.Element => {
if (menuItem.link.type === 'href') {
return (
<a
href={menuItem.link.location}
<Link
to={menuItem.link.location}
className={className}
/>
)
}
return (
<Link
to={menuItem.link.location}
className={className}
let navSubItemElement = (
<TreeNav.SubItem
key={menuItem.id}
id={menuItem.id}
testID={menuItem.testID}
active={getNavItemActivation(
[menuItem.id],
location.pathname
)}
label={menuItem.label}
linkElement={linkElement}
/>
)
}
let navSubItemElement = (
<TreeNav.SubItem
key={menuItem.id}
id={menuItem.id}
testID={menuItem.testID}
active={getNavItemActivation(
[menuItem.id],
location.pathname
)}
label={menuItem.label}
linkElement={linkElement}
/>
)
if (menuItem.cloudExclude) {
navSubItemElement = (
<CloudExclude key={menuItem.id}>
{navSubItemElement}
</CloudExclude>
)
}
if (menuItem.cloudExclude) {
navSubItemElement = (
<CloudExclude key={menuItem.id}>
{navSubItemElement}
</CloudExclude>
)
}
if (menuItem.cloudOnly) {
navSubItemElement = (
<CloudOnly key={menuItem.id}>
{navSubItemElement}
</CloudOnly>
)
}
if (menuItem.cloudOnly) {
navSubItemElement = (
<CloudOnly key={menuItem.id}>
{navSubItemElement}
</CloudOnly>
)
}
if (menuItem.featureFlag) {
navSubItemElement = (
<FeatureFlag
key={menuItem.id}
name={menuItem.featureFlag}
equals={menuItem.featureFlagValue}
>
{navSubItemElement}
</FeatureFlag>
)
}
if (menuItem.featureFlag) {
navSubItemElement = (
<FeatureFlag
key={menuItem.id}
name={menuItem.featureFlag}
equals={menuItem.featureFlagValue}
>
{navSubItemElement}
</FeatureFlag>
)
}
return navSubItemElement
})}
</TreeNav.SubMenu>
)}
</TreeNav.Item>
)
if (item.cloudExclude) {
navItemElement = (
<CloudExclude key={item.id}>{navItemElement}</CloudExclude>
return navSubItemElement
})}
</TreeNav.SubMenu>
)}
</TreeNav.Item>
)
}
if (item.cloudOnly) {
navItemElement = (
<CloudOnly key={item.id}>{navItemElement}</CloudOnly>
)
}
if (item.cloudExclude) {
navItemElement = (
<CloudExclude key={item.id}>{navItemElement}</CloudExclude>
)
}
if (item.featureFlag) {
navItemElement = (
<FeatureFlag
key={item.id}
name={item.featureFlag}
equals={item.featureFlagValue}
>
{navItemElement}
</FeatureFlag>
)
}
if (item.cloudOnly) {
navItemElement = (
<CloudOnly key={item.id}>{navItemElement}</CloudOnly>
)
}
return navItemElement
})}
</TreeNav>
if (item.featureFlag) {
navItemElement = (
<FeatureFlag
key={item.id}
name={item.featureFlag}
equals={item.featureFlagValue}
>
{navItemElement}
</FeatureFlag>
)
}
return navItemElement
})}
</TreeNav>
</OrgSettings>
)
}
}
@ -204,8 +213,18 @@ const mdtp: DispatchProps = {
const mstp = (state: AppState): StateProps => {
const isHidden = get(state, 'app.ephemeral.inPresentationMode', false)
const navBarState = get(state, 'app.persisted.navBarState', 'collapsed')
return {isHidden, navBarState}
const {settings} = get(state, 'cloud.orgSettings')
let showUpgradeButton = false
const hideUpgradeCTA = settings.find(
(setting: OrgSetting) => setting.key === HIDE_UPGRADE_CTA_KEY
)
if (
!hideUpgradeCTA ||
hideUpgradeCTA.value !== PAID_ORG_HIDE_UPGRADE_SETTING.value
) {
showUpgradeButton = true
}
return {isHidden, navBarState, showUpgradeButton}
}
export default connect<StateProps, DispatchProps>(

View File

@ -1,25 +1,56 @@
// Libraries
import React, {FC} from 'react'
import {Link} from 'react-router'
import {connect} from 'react-redux'
// Components
import CloudOnly from 'src/shared/components/cloud/CloudOnly'
// Constants
import {CLOUD_URL, CLOUD_CHECKOUT_PATH} from 'src/shared/constants'
import {
HIDE_UPGRADE_CTA_KEY,
PAID_ORG_HIDE_UPGRADE_SETTING,
} from 'src/cloud/constants'
const CloudUpgradeButton: FC = () => {
// Types
import {AppState, OrgSetting} from 'src/types'
interface StateProps {
show: boolean
}
const CloudUpgradeButton: FC<StateProps> = ({show}) => {
return (
<CloudOnly>
<Link
className="cf-button cf-button-sm cf-button-success upgrade-payg--button"
to={`${CLOUD_URL}${CLOUD_CHECKOUT_PATH}`}
target="_self"
>
Upgrade Now
</Link>
{show ? (
<Link
className="cf-button cf-button-sm cf-button-success upgrade-payg--button"
to={`${CLOUD_URL}${CLOUD_CHECKOUT_PATH}`}
target="_self"
>
Upgrade Now
</Link>
) : null}
</CloudOnly>
)
}
export default CloudUpgradeButton
const mstp = ({
cloud: {
orgSettings: {settings},
},
}: AppState) => {
const hideUpgradeCTA = settings.find(
(setting: OrgSetting) => setting.key === HIDE_UPGRADE_CTA_KEY
)
if (
!hideUpgradeCTA ||
hideUpgradeCTA.value !== PAID_ORG_HIDE_UPGRADE_SETTING.value
) {
return {show: true}
}
return {show: false}
}
export default connect<StateProps>(mstp)(CloudUpgradeButton)

View File

@ -37,6 +37,10 @@ import {membersReducer} from 'src/members/reducers'
import {autoRefreshReducer} from 'src/shared/reducers/autoRefresh'
import {limitsReducer, LimitsState} from 'src/cloud/reducers/limits'
import {demoDataReducer, DemoDataState} from 'src/cloud/reducers/demodata'
import {
orgSettingsReducer,
OrgSettingsState,
} from 'src/cloud/reducers/orgsettings'
import checksReducer from 'src/checks/reducers'
import rulesReducer from 'src/notifications/rules/reducers'
import endpointsReducer from 'src/notifications/endpoints/reducers'
@ -58,9 +62,14 @@ export const rootReducer = combineReducers<ReducerState>({
...sharedReducers,
autoRefresh: autoRefreshReducer,
alertBuilder: alertBuilderReducer,
cloud: combineReducers<{limits: LimitsState; demoData: DemoDataState}>({
cloud: combineReducers<{
limits: LimitsState
demoData: DemoDataState
orgSettings: OrgSettingsState
}>({
limits: limitsReducer,
demoData: demoDataReducer,
orgSettings: orgSettingsReducer,
}),
currentPage: currentPageReducer,
currentDashboard: currentDashboardReducer,

View File

@ -39,3 +39,12 @@ export interface LimitsStatus {
status: string
}
}
export interface OrgSetting {
key: string
value: string
}
export interface OrgSettings {
orgID: string
settings: OrgSetting[]
}

View File

@ -25,6 +25,7 @@ import {LimitsState} from 'src/cloud/reducers/limits'
import {AlertBuilderState} from 'src/alerting/reducers/alertBuilder'
import {CurrentPage} from 'src/shared/reducers/currentPage'
import {DemoDataState} from 'src/cloud/reducers/demodata'
import {OrgSettingsState} from 'src/cloud/reducers/orgsettings'
import {ResourceState} from 'src/types'
@ -32,7 +33,11 @@ export interface AppState {
alertBuilder: AlertBuilderState
app: AppPresentationState
autoRefresh: AutoRefreshState
cloud: {limits: LimitsState; demoData: DemoDataState}
cloud: {
limits: LimitsState
demoData: DemoDataState
orgSettings: OrgSettingsState
}
currentPage: CurrentPage
currentDashboard: CurrentDashboardState
dataLoading: DataLoadingState