diff --git a/ui/src/cloud/actions/orgsettings.ts b/ui/src/cloud/actions/orgsettings.ts new file mode 100644 index 0000000000..44956cae20 --- /dev/null +++ b/ui/src/cloud/actions/orgsettings.ts @@ -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) + } +} diff --git a/ui/src/cloud/apis/orgsettings.ts b/ui/src/cloud/apis/orgsettings.ts new file mode 100644 index 0000000000..a5ef85d9a1 --- /dev/null +++ b/ui/src/cloud/apis/orgsettings.ts @@ -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 + } +} diff --git a/ui/src/cloud/components/OrgSettings.tsx b/ui/src/cloud/components/OrgSettings.tsx new file mode 100644 index 0000000000..87ac96b84d --- /dev/null +++ b/ui/src/cloud/components/OrgSettings.tsx @@ -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) diff --git a/ui/src/cloud/constants/index.ts b/ui/src/cloud/constants/index.ts index 9118e6e493..d4c5a6053b 100644 --- a/ui/src/cloud/constants/index.ts +++ b/ui/src/cloud/constants/index.ts @@ -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', +} diff --git a/ui/src/cloud/reducers/orgsettings.ts b/ui/src/cloud/reducers/orgsettings.ts new file mode 100644 index 0000000000..a0f1327f56 --- /dev/null +++ b/ui/src/cloud/reducers/orgsettings.ts @@ -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 + }) diff --git a/ui/src/pageLayout/containers/TreeNav.tsx b/ui/src/pageLayout/containers/TreeNav.tsx index 4f66edf7aa..ee0682a798 100644 --- a/ui/src/pageLayout/containers/TreeNav.tsx +++ b/ui/src/pageLayout/containers/TreeNav.tsx @@ -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>( diff --git a/ui/src/shared/components/CloudUpgradeButton.tsx b/ui/src/shared/components/CloudUpgradeButton.tsx index 4e06709d3f..84ce91f79f 100644 --- a/ui/src/shared/components/CloudUpgradeButton.tsx +++ b/ui/src/shared/components/CloudUpgradeButton.tsx @@ -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) diff --git a/ui/src/store/configureStore.ts b/ui/src/store/configureStore.ts index 803be750ef..01e4dd55d7 100644 --- a/ui/src/store/configureStore.ts +++ b/ui/src/store/configureStore.ts @@ -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, diff --git a/ui/src/types/cloud.ts b/ui/src/types/cloud.ts index e6a31ad6f2..5a7068dc7d 100644 --- a/ui/src/types/cloud.ts +++ b/ui/src/types/cloud.ts @@ -39,3 +39,12 @@ export interface LimitsStatus { status: string } } +export interface OrgSetting { + key: string + value: string +} + +export interface OrgSettings { + orgID: string + settings: OrgSetting[] +} diff --git a/ui/src/types/stores.ts b/ui/src/types/stores.ts index b3046fb184..2e225ed033 100644 --- a/ui/src/types/stores.ts +++ b/ui/src/types/stores.ts @@ -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