feat: Code-split for admin setting for toggling appsmith watermark (#15036)

* create checkbox

* code split for hiding branding badge

* code split for hiding branding badge

* code review feedback fixes

* rename auth to upgrade button

* update mobile branding badge

* change label

* minor refactor

* minor updates

* add a cypress tests to check if watermark setting contains a upgrade button
This commit is contained in:
Pawan Kumar 2022-07-28 14:08:37 +05:30 committed by GitHub
parent a02155fc88
commit bae0b75583
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 325 additions and 91 deletions

View File

@ -32,4 +32,13 @@ describe("Admin settings page", function() {
.should("contain", "UPGRADE");
}
});
it.only("should test that Appsmith Watermark setting shows upgrade button", () => {
cy.visit("/settings/general");
// checking if the setting contains a word 'Upgrade
cy.get(
EnterpriseAdminSettingsLocators.hideAppsmithWatermarkSetting,
).contains("Upgrade");
});
});

View File

@ -1,4 +1,5 @@
{
"upgradeSamlButton": ".t--settings-sub-category-upgrade-saml",
"upgradeOidcButton": ".t--settings-sub-category-upgrade-oidc"
}
"upgradeOidcButton": ".t--settings-sub-category-upgrade-oidc",
"hideAppsmithWatermarkSetting": ".admin-settings-group-appsmith-hide-watermark"
}

View File

@ -44,6 +44,7 @@ server {
sub_filter __APPSMITH_FORM_LOGIN_DISABLED__ '${APPSMITH_FORM_LOGIN_DISABLED}';
sub_filter __APPSMITH_SIGNUP_DISABLED__ '${APPSMITH_SIGNUP_DISABLED}';
sub_filter __APPSMITH_ZIPY_SDK_KEY__ '${APPSMITH_ZIPY_SDK_KEY}';
sub_filter __APPSMITH_HIDE_WATERMARK__ '${APPSMITH_HIDE_WATERMARK}';
}
location /api {

View File

@ -54,6 +54,7 @@ server {
sub_filter __APPSMITH_FORM_LOGIN_DISABLED__ '${APPSMITH_FORM_LOGIN_DISABLED}';
sub_filter __APPSMITH_SIGNUP_DISABLED__ '${APPSMITH_SIGNUP_DISABLED}';
sub_filter __APPSMITH_ZIPY_SDK_KEY__ '${APPSMITH_ZIPY_SDK_KEY}';
sub_filter __APPSMITH_HIDE_WATERMARK__ '${APPSMITH_HIDE_WATERMARK}';
}

View File

@ -52,6 +52,7 @@ server {
sub_filter __APPSMITH_FORM_LOGIN_DISABLED__ '${APPSMITH_FORM_LOGIN_DISABLED}';
sub_filter __APPSMITH_SIGNUP_DISABLED__ '${APPSMITH_SIGNUP_DISABLED}';
sub_filter __APPSMITH_ZIPY_SDK_KEY__ '${APPSMITH_ZIPY_SDK_KEY}';
sub_filter __APPSMITH_HIDE_WATERMARK__ '${APPSMITH_HIDE_WATERMARK}';
}

View File

@ -77,6 +77,7 @@ module.exports = {
mailEnabled: parseConfig("__APPSMITH_MAIL_ENABLED__"),
disableTelemetry: "DISABLE_TELEMETRY" === "" || "DISABLE_TELEMETRY",
hideWatermark: parseConfig("__APPSMITH_HIDE_WATERMARK__"),
},
},
};

View File

@ -211,6 +211,7 @@
mailEnabled: parseConfig("__APPSMITH_MAIL_ENABLED__"),
cloudServicesBaseUrl: parseConfig("__APPSMITH_CLOUD_SERVICES_BASE_URL__") || "https://cs.appsmith.com",
googleRecaptchaSiteKey: parseConfig("__APPSMITH_RECAPTCHA_SITE_KEY__"),
hideWatermark: parseConfig("__APPSMITH_HIDE_WATERMARK__")
};
</script>
</body>

View File

@ -46,6 +46,7 @@ export interface INJECTED_CONFIGS {
cloudServicesBaseUrl: string;
googleRecaptchaSiteKey: string;
supportEmail: string;
hideWatermark: boolean;
}
const capitalizeText = (text: string) => {
@ -124,6 +125,9 @@ export const getConfigsFromEnvVars = (): INJECTED_CONFIGS => {
googleRecaptchaSiteKey:
process.env.REACT_APP_GOOGLE_RECAPTCHA_SITE_KEY || "",
supportEmail: process.env.APPSMITH_SUPPORT_EMAIL || "support@appsmith.com",
hideWatermark: process.env.REACT_APP_APPSMITH_HIDE_WATERMARK
? process.env.REACT_APP_APPSMITH_HIDE_WATERMARK.length > 0
: false,
};
};
@ -270,5 +274,7 @@ export const getAppsmithConfigs = (): AppsmithUIConfigs => {
ENV_CONFIG.cloudServicesBaseUrl ||
APPSMITH_FEATURE_CONFIGS.cloudServicesBaseUrl,
appsmithSupportEmail: ENV_CONFIG.supportEmail,
hideWatermark:
ENV_CONFIG.hideWatermark || APPSMITH_FEATURE_CONFIGS.hideWatermark,
};
};

View File

@ -68,4 +68,5 @@ export interface AppsmithUIConfigs {
apiKey: string;
};
appsmithSupportEmail: string;
hideWatermark: boolean;
}

View File

@ -1102,6 +1102,7 @@ export const APP_THEME_BETA_CARD_CONTENT = () =>
export const UPGRADE_TO_EE = (authLabel: string) =>
`Hello, I would like to upgrade and start using ${authLabel} authentication.`;
export const UPGRADE_TO_EE_GENERIC = () => `Hello, I would like to upgrade`;
export const ADMIN_AUTH_SETTINGS_TITLE = () => "Select Authentication Method";
export const ADMIN_AUTH_SETTINGS_SUBTITLE = () =>
"Select a protocol you want to authenticate users with";

View File

@ -0,0 +1,96 @@
import React from "react";
import { isEmail } from "utils/formhelpers";
import { apiRequestConfig } from "api/Api";
import UserApi from "@appsmith/api/UserApi";
import {
AdminConfigType,
SettingCategories,
SettingSubtype,
SettingTypes,
Setting,
} from "@appsmith/pages/AdminSettings/config/types";
import BrandingBadge from "pages/AppViewer/BrandingBadge";
export const APPSMITH_INSTANCE_NAME_SETTING_SETTING: Setting = {
id: "APPSMITH_INSTANCE_NAME",
category: SettingCategories.GENERAL,
controlType: SettingTypes.TEXTINPUT,
controlSubType: SettingSubtype.TEXT,
label: "Instance Name",
placeholder: "appsmith/prod",
};
export const APPSMITH__ADMIN_EMAILS_SETTING: Setting = {
id: "APPSMITH_ADMIN_EMAILS",
category: SettingCategories.GENERAL,
controlType: SettingTypes.TEXTINPUT,
controlSubType: SettingSubtype.EMAIL,
label: "Admin Email",
subText:
"Emails of the users who can modify instance settings (Comma Separated)",
placeholder: "Jane@example.com",
validate: (value: string) => {
if (
value &&
!value
.split(",")
.reduce((prev, curr) => prev && isEmail(curr.trim()), true)
) {
return "Please enter valid email id(s)";
}
},
};
export const APPSMITH_DOWNLOAD_DOCKER_COMPOSE_FILE_SETTING: Setting = {
id: "APPSMITH_DOWNLOAD_DOCKER_COMPOSE_FILE",
action: () => {
const { host, protocol } = window.location;
window.open(
`${protocol}//${host}${apiRequestConfig.baseURL}${UserApi.downloadConfigURL}`,
"_blank",
);
},
category: SettingCategories.GENERAL,
controlType: SettingTypes.BUTTON,
label: "Generated Docker Compose File",
text: "Download",
};
export const APPSMITH_DISABLE_TELEMETRY_SETTING: Setting = {
id: "APPSMITH_DISABLE_TELEMETRY",
category: SettingCategories.GENERAL,
controlType: SettingTypes.TOGGLE,
label: "Share anonymous usage data",
subText: "Share anonymous usage data to help improve the product",
toggleText: (value: boolean) =>
value ? "Don't share any data" : "Share Anonymous Telemetry",
};
export const APPSMITH_HIDE_WATERMARK_SETTING: Setting = {
id: "APPSMITH_HIDE_WATERMARK",
name: "appsmith-hide-watermark",
category: SettingCategories.GENERAL,
controlType: SettingTypes.CHECKBOX,
label: "Appsmith Watermark",
text: "Show Appsmith Watermark",
needsUpgrade: true,
isDisabled: () => true,
textSuffix: <BrandingBadge />,
upgradeLogEventName: "ADMIN_SETTINGS_UPGRADE_WATERMARK",
upgradeIntercomMessage:
"Hello, I would like to upgrade and remove the watermark.",
};
export const config: AdminConfigType = {
type: SettingCategories.GENERAL,
controlType: SettingTypes.GROUP,
title: "General",
canSave: true,
settings: [
APPSMITH_INSTANCE_NAME_SETTING_SETTING,
APPSMITH__ADMIN_EMAILS_SETTING,
APPSMITH_DOWNLOAD_DOCKER_COMPOSE_FILE_SETTING,
APPSMITH_DISABLE_TELEMETRY_SETTING,
APPSMITH_HIDE_WATERMARK_SETTING,
],
} as AdminConfigType;

View File

@ -1,6 +1,6 @@
import { ConfigFactory } from "pages/Settings/config/ConfigFactory";
import { config as GeneralConfig } from "pages/Settings/config/general";
import { config as GeneralConfig } from "@appsmith/pages/AdminSettings/config/general";
import { config as EmailConfig } from "pages/Settings/config/email";
import { config as MapsConfig } from "pages/Settings/config/googleMaps";
import { config as VersionConfig } from "pages/Settings/config/version";

View File

@ -1,6 +1,7 @@
import React from "react";
import { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
import { Dispatch } from "react";
import { EventName } from "utils/AnalyticsUtil";
export enum SettingTypes {
TEXTINPUT = "TEXTINPUT",
@ -14,6 +15,7 @@ export enum SettingTypes {
ACCORDION = "ACCORDION",
TAGINPUT = "TAGINPUT",
DROPDOWN = "DROPDOWN",
CHECKBOX = "CHECKBOX",
}
export enum SettingSubtype {
@ -38,6 +40,7 @@ export interface Setting {
subCategory?: string;
value?: string;
text?: string;
textSuffix?: React.ReactElement;
action?: (
dispatch: Dispatch<ReduxAction<any>>,
settings?: Record<string, any>,
@ -54,6 +57,9 @@ export interface Setting {
formName?: string;
fieldName?: string;
dropdownOptions?: Array<{ id: string; value: string; label?: string }>;
needsUpgrade?: boolean;
upgradeLogEventName?: EventName;
upgradeIntercomMessage?: string;
}
export interface Category {

View File

@ -63,10 +63,7 @@ export const getDefaultRole = createSelector(
return roles?.find((role) => role.isDefault);
},
);
export const getCurrentError = (state: AppState) => {
return state.ui.errors.currentError;
};
export const getShowBrandingBadge = () => {
return true;
};

View File

@ -0,0 +1 @@
export * from "ce/pages/AdminSettings/config/general";

View File

@ -4,15 +4,10 @@ import { ReactComponent as AppsmithLogo } from "assets/svg/appsmith-logo-no-pad.
function BrandingBadge() {
return (
<a
className="fixed items-center hidden p-1 px-2 space-x-2 bg-white border rounded-md md:flex z-2 hover:no-underline right-8 bottom-4 backdrop-blur-xl backdrop-filter"
href="https://appsmith.com"
rel="noreferrer"
target="_blank"
>
<span className="flex items-center p-1 px-2 space-x-2 bg-white border rounded-md w-max backdrop-blur-xl backdrop-filter">
<h4 className="text-xs text-gray-500">Built on</h4>
<AppsmithLogo className="w-auto h-3" />
</a>
</span>
);
}

View File

@ -19,7 +19,7 @@ import { getSelectedAppTheme } from "selectors/appThemingSelectors";
import BrandingBadge from "./BrandingBadgeMobile";
import { getAppViewHeaderHeight } from "selectors/appViewSelectors";
import { useOnClickOutside } from "utils/hooks/useOnClickOutside";
import { getShowBrandingBadge } from "@appsmith/selectors/workspaceSelectors";
import { getAppsmithConfigs } from "@appsmith/configs";
import { useHref } from "pages/Editor/utils";
import { APP_MODE } from "entities/App";
import { builderURL, viewerURL } from "RouteBuilder";
@ -44,7 +44,7 @@ export function PageMenu(props: AppViewerHeaderProps) {
);
const headerHeight = useSelector(getAppViewHeaderHeight);
const [query, setQuery] = useState("");
const showBrandingBadge = useSelector(getShowBrandingBadge);
const { hideWatermark } = getAppsmithConfigs();
// hide menu on click outside
useOnClickOutside(
@ -91,11 +91,12 @@ export function PageMenu(props: AppViewerHeaderProps) {
"-left-full": !isOpen,
"left-0": isOpen,
})}
ref={menuRef}
style={{
height: `calc(100% - ${headerHeight}px)`,
}}
>
<div className="flex-grow py-3 overflow-y-auto" ref={menuRef}>
<div className="flex-grow py-3 overflow-y-auto">
{appPages.map((page) => (
<PageNavLink key={page.pageId} page={page} query={query} />
))}
@ -128,7 +129,16 @@ export function PageMenu(props: AppViewerHeaderProps) {
/>
)}
<PrimaryCTA className="t--back-to-editor--mobile" url={props.url} />
{showBrandingBadge && <BrandingBadge />}
{!hideWatermark && (
<a
className="flex hover:no-underline"
href="https://appsmith.com"
rel="noreferrer"
target="_blank"
>
<BrandingBadge />
</a>
)}
</div>
</div>
</>

View File

@ -41,12 +41,12 @@ import {
import { setAppViewHeaderHeight } from "actions/appViewActions";
import { showPostCompletionMessage } from "selectors/onboardingSelectors";
import { CANVAS_SELECTOR } from "constants/WidgetConstants";
import { getShowBrandingBadge } from "@appsmith/selectors/workspaceSelectors";
import { fetchPublishedPage } from "actions/pageActions";
import usePrevious from "utils/hooks/usePrevious";
import { getIsBranchUpdated } from "../utils";
import { APP_MODE } from "entities/App";
import { initAppViewer } from "actions/initActions";
import { getAppsmithConfigs } from "@appsmith/configs";
const AppViewerBody = styled.section<{
hasPages: boolean;
@ -97,9 +97,9 @@ function AppViewer(props: Props) {
);
const showGuidedTourMessage = useSelector(showPostCompletionMessage);
const headerHeight = useSelector(getAppViewHeaderHeight);
const showBrandingBadge = useSelector(getShowBrandingBadge);
const branch = getSearchQuery(search, GIT_BRANCH_QUERY_KEY);
const prevValues = usePrevious({ branch, location: props.location, pageId });
const { hideWatermark } = getAppsmithConfigs();
/**
* initializes the widgets factory and registers all widgets
@ -262,7 +262,16 @@ function AppViewer(props: Props) {
>
{isInitialized && registered && <AppViewerPageContainer />}
</AppViewerBody>
{showBrandingBadge && <BrandingBadge />}
{!hideWatermark && (
<a
className="fixed hidden right-8 bottom-4 z-2 hover:no-underline md:flex"
href="https://appsmith.com"
rel="noreferrer"
target="_blank"
>
<BrandingBadge />
</a>
)}
</AppViewerBodyContainer>
</ContainerWithComments>
<AddCommentTourComponent />

View File

@ -0,0 +1,120 @@
import React, { memo } from "react";
import {
Field,
getFormValues,
WrappedFieldInputProps,
WrappedFieldMetaProps,
} from "redux-form";
import styled from "styled-components";
import { FormGroup, SettingComponentProps } from "./Common";
import { FormTextFieldProps } from "components/ads/formFields/TextField";
import Checkbox from "components/ads/Checkbox";
import { Button, Category } from "components/ads";
import { useSelector } from "react-redux";
import { SETTINGS_FORM_NAME } from "constants/forms";
import useOnUpgrade from "utils/hooks/useOnUpgrade";
import { EventName } from "utils/AnalyticsUtil";
const CheckboxWrapper = styled.div`
display: grid;
margin-bottom: 8px;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 16px;
`;
const UpgradeButton = styled(Button)`
height: 30px;
width: 94px;
padding: 8px 16px;
`;
type CheckboxProps = {
label?: string;
id?: string;
isDisabled?: boolean;
needsUpgrade?: boolean;
text: string;
labelSuffix?: React.ReactElement;
upgradeLogEventName?: EventName;
upgradeIntercomMessage?: string;
isPropertyDisabled?: boolean;
};
function FieldCheckboxWithCheckboxText(props: CheckboxProps) {
return function FieldCheckbox(
componentProps: FormTextFieldProps & {
meta: Partial<WrappedFieldMetaProps>;
input: Partial<WrappedFieldInputProps>;
},
) {
const { isPropertyDisabled, labelSuffix } = props;
const val = componentProps.input.value;
const { onUpgrade } = useOnUpgrade({
logEventName: props.upgradeLogEventName,
intercomMessage: props.upgradeIntercomMessage,
});
function onCheckbox(value?: boolean) {
const CheckboxValue = isPropertyDisabled ? !value : value;
componentProps.input.onChange &&
componentProps.input.onChange(CheckboxValue);
componentProps.input.onBlur && componentProps.input.onBlur(CheckboxValue);
}
/* Value = !ENV_VARIABLE
This has been done intentionally as naming convention used contains the word disabled but the UI should show the button enabled by default.
*/
return (
<CheckboxWrapper>
<Checkbox
cypressSelector={props.id}
disabled={props.isDisabled}
isDefaultChecked={isPropertyDisabled ? !val : val}
label={props.text}
onCheckChange={onCheckbox}
/>
<div>{labelSuffix}</div>
{props.needsUpgrade && (
<UpgradeButton
category={Category.tertiary}
onClick={onUpgrade}
text="Upgrade"
/>
)}
</CheckboxWrapper>
);
};
}
const StyledFieldCheckboxGroup = styled.div`
margin-bottom: 8px;
`;
const formValuesSelector = getFormValues(SETTINGS_FORM_NAME);
export function CheckboxComponent({ setting }: SettingComponentProps) {
const settings = useSelector(formValuesSelector);
return (
<StyledFieldCheckboxGroup>
<FormGroup setting={setting}>
<Field
component={FieldCheckboxWithCheckboxText({
label: setting.label,
text: setting.text || "",
id: setting.id,
isDisabled: setting.isDisabled && setting.isDisabled(settings),
needsUpgrade: setting.needsUpgrade,
labelSuffix: setting.textSuffix,
upgradeLogEventName: setting.upgradeLogEventName,
upgradeIntercomMessage: setting.upgradeIntercomMessage,
isPropertyDisabled: !setting.name?.toLowerCase().includes("enable"),
})}
name={setting.name}
/>
</FormGroup>
</StyledFieldCheckboxGroup>
);
}
export default memo(CheckboxComponent);

View File

@ -33,7 +33,6 @@ export const StyledFormGroup = styled.div`
& svg:hover {
cursor: default;
path {
fill: #fff;
}
}
`;

View File

@ -23,6 +23,7 @@ import TagInputField from "./TagInputField";
import Dropdown from "./Dropdown";
import { Classes } from "@blueprintjs/core";
import { Colors } from "constants/Colors";
import Checkbox from "./Checkbox";
type GroupProps = {
name?: string;
@ -126,6 +127,17 @@ export default function Group({
<Toggle setting={setting} />
</div>
);
case SettingTypes.CHECKBOX:
return (
<div
className={`admin-settings-group-${setting.name ||
setting.id} ${setting.isHidden ? "hide" : ""}`}
data-testid="admin-settings-group-checkbox"
key={setting.name || setting.id}
>
<Checkbox setting={setting} />
</div>
);
case SettingTypes.LINK:
return (
<div

View File

@ -1,69 +0,0 @@
import { isEmail } from "utils/formhelpers";
import { apiRequestConfig } from "api/Api";
import UserApi from "@appsmith/api/UserApi";
import {
AdminConfigType,
SettingCategories,
SettingSubtype,
SettingTypes,
} from "@appsmith/pages/AdminSettings/config/types";
export const config: AdminConfigType = {
type: SettingCategories.GENERAL,
controlType: SettingTypes.GROUP,
title: "General",
canSave: true,
settings: [
{
id: "APPSMITH_INSTANCE_NAME",
category: SettingCategories.GENERAL,
controlType: SettingTypes.TEXTINPUT,
controlSubType: SettingSubtype.TEXT,
label: "Instance Name",
placeholder: "appsmith/prod",
},
{
id: "APPSMITH_ADMIN_EMAILS",
category: SettingCategories.GENERAL,
controlType: SettingTypes.TEXTINPUT,
controlSubType: SettingSubtype.EMAIL,
label: "Admin Email",
subText:
"Emails of the users who can modify instance settings (Comma Separated)",
placeholder: "Jane@example.com",
validate: (value: string) => {
if (
value &&
!value
.split(",")
.reduce((prev, curr) => prev && isEmail(curr.trim()), true)
) {
return "Please enter valid email id(s)";
}
},
},
{
id: "APPSMITH_DOWNLOAD_DOCKER_COMPOSE_FILE",
action: () => {
const { host, protocol } = window.location;
window.open(
`${protocol}//${host}${apiRequestConfig.baseURL}${UserApi.downloadConfigURL}`,
"_blank",
);
},
category: SettingCategories.GENERAL,
controlType: SettingTypes.BUTTON,
label: "Generated Docker Compose File",
text: "Download",
},
{
id: "APPSMITH_DISABLE_TELEMETRY",
category: SettingCategories.GENERAL,
controlType: SettingTypes.TOGGLE,
label: "Share anonymous usage data",
subText: "Share anonymous usage data to help improve the product",
toggleText: (value: boolean) =>
value ? "Don't share any data" : "Share Anonymous Telemetry",
},
],
} as AdminConfigType;

View File

@ -248,6 +248,8 @@ export type EventName =
| "BACK_BUTTON_CLICK"
| "WIDGET_TAB_CLICK"
| "ENTITY_EXPLORER_CLICK"
| "ADMIN_SETTINGS_UPGRADE_WATERMARK"
| "ADMIN_SETTINGS_UPGRADE"
| "PRETTIFY_CODE_MANUAL_TRIGGER"
| "PRETTIFY_CODE_KEYBOARD_SHORTCUT";

View File

@ -0,0 +1,33 @@
import { getAppsmithConfigs } from "@appsmith/configs";
import { createMessage, UPGRADE_TO_EE_GENERIC } from "ce/constants/messages";
import AnalyticsUtil, { EventName } from "utils/AnalyticsUtil";
const { intercomAppID } = getAppsmithConfigs();
type Props = {
intercomMessage?: string;
logEventName?: EventName;
logEventData?: any;
};
const useOnUpgrade = (props: Props) => {
const { intercomMessage, logEventData, logEventName } = props;
const triggerIntercom = (message: string) => {
if (intercomAppID && window.Intercom) {
window.Intercom("showNewMessage", message);
}
};
const onUpgrade = () => {
AnalyticsUtil.logEvent(
logEventName || "ADMIN_SETTINGS_UPGRADE",
logEventData,
);
triggerIntercom(intercomMessage || createMessage(UPGRADE_TO_EE_GENERIC));
};
return { onUpgrade };
};
export default useOnUpgrade;