feat: Cloud billing static UI (#39846)

## Description

This PR contains static UI for cloud billing.

https://www.figma.com/design/XouAwUQJKF2lf57bQvNM1e/Cloud-Billing-(-Phase-1-)?node-id=15907-14530&t=ogZ4sTrvsBdvVL8m-0


Fixes #https://github.com/appsmithorg/appsmith/issues/39896

## Automation

/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!CAUTION]
> 🔴 🔴 🔴 Some tests have failed.
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/14077392797>
> Commit: 73f7f2d1bee5a6bf3547af050c1fa6172052cc6e
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=14077392797&attempt=2&selectiontype=test&testsstatus=failed&specsstatus=fail"
target="_blank">Cypress dashboard</a>.
> Tags: @tag.All
> Spec: 
> The following are new failures, please fix them before merging the PR:
<ol>
>
<li>cypress/e2e/Regression/ClientSide/FormLogin/EnableFormLogin_spec.js</ol>
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/identified-flaky-tests-65890b3c81d7400d08fa9ee3?branch=master"
target="_blank">List of identified flaky tests</a>.
> <hr>Wed, 26 Mar 2025 08:34:53 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
- Introduced a postfix functionality for input fields, allowing
additional content to be displayed alongside the standard input.
- Enhanced styling to dynamically adjust input spacing when a postfix is
provided.
- Extended form input components to support the optional postfix
property for greater flexibility in content presentation.
  - Added a feature flag for multi-organization licensing management.
  - Introduced a custom hook to check if cloud billing is enabled.
- Added new user interface messages for account management in the signup
process.
  - Implemented a new route for organizational login.
- Added a conditional input field for full name based on cloud billing
status.

- **Bug Fixes**
- Improved rendering logic for end icons and postfix elements in input
fields.

- **Documentation**
- Updated export statements to streamline access to new hooks and
selectors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
albinAppsmith 2025-03-26 14:17:46 +05:30 committed by GitHub
parent dc079f0d8d
commit e3a49845e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 146 additions and 27 deletions

View File

@ -9,3 +9,4 @@ export const InputStartIconClassName = `${InputIconClassName}-start`;
export const InputEndIconClassName = `${InputIconClassName}-end`;
export const InputStartIconDisabledClassName = `${InputStartIconClassName}-disabled`;
export const InputEndIconDisabledClassName = `${InputEndIconClassName}-disabled`;
export const InputPostfixClassName = `${InputClassName}-postfix`;

View File

@ -5,6 +5,7 @@ import {
InputIconClassName,
InputStartIconClassName,
InputStartIconDisabledClassName,
InputPostfixClassName,
} from "./Input.constants";
import type { InputSizes } from "./Input.types";
import { Text } from "../Text";
@ -129,6 +130,15 @@ export const InputContainer = styled.div<{
opacity: var(--ads-v2-opacity-disabled);
cursor: not-allowed !important;
}
.${InputPostfixClassName} {
position: absolute;
right: var(--ads-v2-spaces-3);
display: flex;
align-items: center;
pointer-events: none;
color: var(--ads-v2-colors-content-label-default);
}
`;
export const StyledInput = styled.input<{
@ -139,6 +149,8 @@ export const StyledInput = styled.input<{
hasEndIcon?: boolean;
renderer?: "input" | "textarea";
inputSize?: InputSizes;
hasPostfix?: boolean;
postfixSize?: number;
}>`
--icon-size: ${({ inputSize }) => inputSize && iconSizes[inputSize]};
@ -176,6 +188,13 @@ export const StyledInput = styled.input<{
);
`}
/* Additional padding for postfix */
${({ hasPostfix, postfixSize }) =>
hasPostfix &&
css`
padding-right: calc(var(--input-padding-x) + ${postfixSize}ch);
`}
&:hover:enabled:not(:read-only) {
--input-color-border: var(--ads-v2-colors-control-field-hover-border);
}

View File

@ -22,6 +22,7 @@ import {
InputEndIconClassName,
InputIconClassName,
InputStartIconClassName,
InputPostfixClassName,
} from "./Input.constants";
const Input = forwardRef<HTMLInputElement, InputProps>(
@ -40,6 +41,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
label,
labelPosition = "top",
onChange,
postfix,
renderAs = "input",
size = "sm",
startIcon,
@ -125,30 +127,47 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
data-is-valid={isValid}
disabled={disableTextInput || isDisabled}
hasEndIcon={!!endIcon}
hasPostfix={!!postfix}
hasStartIcon={!!startIcon}
inputSize={size}
onChange={handleOnChange}
postfixSize={postfix?.length}
readOnly={isReadOnly}
ref={inputRef}
renderer={renderAs}
value={value}
{...rest}
/>
{/* End Icon Section */}
{endIcon && renderAs === "input" ? (
<Icon
className={clsx(
InputIconClassName,
InputEndIconClassName,
endIconClassName,
{/* End Icon/Postfix Section */}
{renderAs === "input" && (
<>
{postfix && (
<span
className={clsx(InputPostfixClassName)}
style={{
color: "var(--ads-v2-colors-content-label-default)",
userSelect: "none",
}}
>
{postfix}
</span>
)}
data-has-onclick={!!endIconOnClick}
name={endIcon}
onClick={endIconOnClick}
size={size}
{...restOfEndIconProps}
/>
) : null}
{endIcon && (
<Icon
className={clsx(
InputIconClassName,
InputEndIconClassName,
endIconClassName,
)}
data-has-onclick={!!endIconOnClick}
name={endIcon}
onClick={endIconOnClick}
size={size}
{...restOfEndIconProps}
/>
)}
</>
)}
</InputContainer>
{description && (
<Description

View File

@ -27,6 +27,8 @@ interface Props extends TextFieldProps {
labelPosition?: "top" | "left";
/** name */
name?: string;
/** postfix */
postfix?: string;
/** start icon */
startIcon?: string;
/** start icon props */

View File

@ -86,6 +86,9 @@ export const SIGNUP_PAGE_SUBMIT_BUTTON_TEXT = () => `Sign up`;
export const ALREADY_HAVE_AN_ACCOUNT = () => `Already have an account?`;
export const LOOKING_TO_SELF_HOST = () => "Looking to self-host Appsmith?";
export const VISIT_OUR_DOCS = () => "Visit our docs";
export const ALREADY_USING_APPSMITH = () => `Already using Appsmith?`;
export const SIGN_IN_TO_AN_EXISTING_ORGANISATION = () =>
`Sign in to an existing organisation`;
export const SIGNUP_PAGE_SUCCESS = () =>
`Awesome! You have successfully registered.`;
@ -1576,6 +1579,7 @@ export const WELCOME_FORM_NON_SUPER_USER_USE_CASE = () =>
"What would you like to use Appsmith for?";
export const WELCOME_FORM_NON_SUPER_USER_PROFICIENCY_LEVEL = () =>
"What is your general development proficiency?";
export const WELCOME_FORM_FULL_NAME = () => "Whats your full name?";
export const WELCOME_FORM_PROFICIENCY_ERROR_MESSAGE = () =>
"Please select a proficiency level";

View File

@ -53,6 +53,7 @@ export const FEATURE_FLAG = {
"release_external_saas_plugins_enabled",
release_tablev2_infinitescroll_enabled:
"release_tablev2_infinitescroll_enabled",
license_multi_org_enabled: "license_multi_org_enabled",
release_table_custom_sort_function_enabled:
"release_table_custom_sort_function_enabled",
} as const;
@ -99,6 +100,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
release_ads_entity_item_enabled: false,
release_external_saas_plugins_enabled: false,
release_tablev2_infinitescroll_enabled: false,
license_multi_org_enabled: false,
release_table_custom_sort_function_enabled: false,
};

View File

@ -65,6 +65,7 @@ export interface FormTextFieldProps {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parse?: (value: any) => any;
postfix?: string;
}
function ReduxFormTextField(props: FormTextFieldProps) {

View File

@ -29,6 +29,7 @@ export const WORKSPACE_SETTINGS_PAGE_URL = `${WORKSPACE_URL}/settings`;
export const WORKSPACE_SETTINGS_GENERAL_PAGE_URL = `${WORKSPACE_URL}/settings/general`;
export const WORKSPACE_SETTINGS_MEMBERS_PAGE_URL = `${WORKSPACE_URL}/settings/members`;
export const WORKSPACE_SETTINGS_LICENSE_PAGE_URL = `/settings/license`;
export const ORG_LOGIN_PATH = "/org";
export const matchApplicationPath = match(APPLICATIONS_URL);
export const matchTemplatesPath = match(TEMPLATES_PATH);

View File

@ -0,0 +1 @@
export * from "./useIsCloudBillingEnabled";

View File

@ -0,0 +1,15 @@
import { useSelector } from "react-redux";
import { getAppsmithConfigs } from "ee/configs";
import { getIsCloudBillingFeatureFlagEnabled } from "selectors/cloudBillingSelectors";
const useIsCloudBillingEnabled = () => {
const { cloudHosting } = getAppsmithConfigs();
const isCloudBillingFeatureFlagEnabled = useSelector(
getIsCloudBillingFeatureFlagEnabled,
);
return isCloudBillingFeatureFlagEnabled && cloudHosting;
};
export { useIsCloudBillingEnabled };

View File

@ -1,7 +1,7 @@
import React, { useEffect } from "react";
import type { InjectedFormProps } from "redux-form";
import { reduxForm, formValueSelector } from "redux-form";
import { AUTH_LOGIN_URL } from "constants/routes";
import { AUTH_LOGIN_URL, ORG_LOGIN_PATH } from "constants/routes";
import { SIGNUP_FORM_NAME } from "ee/constants/forms";
import type { RouteComponentProps } from "react-router-dom";
import { useHistory, useLocation, withRouter } from "react-router-dom";
@ -26,6 +26,8 @@ import {
GOOGLE_RECAPTCHA_KEY_ERROR,
LOOKING_TO_SELF_HOST,
VISIT_OUR_DOCS,
ALREADY_USING_APPSMITH,
SIGN_IN_TO_AN_EXISTING_ORGANISATION,
} from "ee/constants/messages";
import FormTextField from "components/utils/ReduxFormTextField";
import ThirdPartyAuth from "pages/UserAuth/ThirdPartyAuth";
@ -59,6 +61,8 @@ import log from "loglevel";
import { SELF_HOSTING_DOC } from "constants/ThirdPartyConstants";
import * as Sentry from "@sentry/react";
import CsrfTokenInput from "pages/UserAuth/CsrfTokenInput";
import { useIsCloudBillingEnabled } from "hooks";
import { isLoginHostname } from "utils/cloudBillingUtils";
declare global {
interface Window {
@ -122,6 +126,8 @@ export function SignUp(props: SignUpFormProps) {
const organizationConfig = useSelector(getOrganizationConfig);
const { instanceName } = organizationConfig;
const htmlPageTitle = getHTMLPageTitle(isBrandingEnabled, instanceName);
const isCloudBillingEnabled = useIsCloudBillingEnabled();
const isHostnameEqualtoLogin = isLoginHostname();
const recaptchaStatus = useScript(
`https://www.google.com/recaptcha/api.js?render=${googleRecaptchaSiteKey.apiKey}`,
@ -195,17 +201,31 @@ export function SignUp(props: SignUpFormProps) {
const footerSection = (
<>
<div className="px-2 flex align-center justify-center text-center text-[color:var(--ads-v2\-color-fg)] text-[14px]">
{createMessage(ALREADY_HAVE_AN_ACCOUNT)}&nbsp;
<Link
className="t--sign-up t--signup-link"
kind="primary"
target="_self"
to={AUTH_LOGIN_URL}
>
{createMessage(SIGNUP_PAGE_LOGIN_LINK_TEXT)}
</Link>
</div>
{isCloudBillingEnabled && isHostnameEqualtoLogin ? (
<div className="px-2 flex flex-col items-center justify-center text-center text-[color:var(--ads-v2\-color-fg)] text-[14px]">
{createMessage(ALREADY_USING_APPSMITH)}
<Link
className="t--sign-up t--signup-link"
kind="primary"
target="_self"
to={ORG_LOGIN_PATH}
>
{createMessage(SIGN_IN_TO_AN_EXISTING_ORGANISATION)}
</Link>
</div>
) : (
<div className="px-2 flex align-center justify-center text-center text-[color:var(--ads-v2\-color-fg)] text-[14px]">
{createMessage(ALREADY_HAVE_AN_ACCOUNT)}&nbsp;
<Link
className="t--sign-up t--signup-link"
kind="primary"
target="_self"
to={AUTH_LOGIN_URL}
>
{createMessage(SIGNUP_PAGE_LOGIN_LINK_TEXT)}
</Link>
</div>
)}
{cloudHosting && (
<>
<OrWithLines>or</OrWithLines>

View File

@ -12,6 +12,7 @@ import {
WELCOME_FORM_NON_SUPER_USER_PROFICIENCY_LEVEL,
WELCOME_FORM_PROFICIENCY_ERROR_MESSAGE,
WELCOME_FORM_USE_CASE_ERROR_MESSAGE,
WELCOME_FORM_FULL_NAME,
} from "ee/constants/messages";
import { connect } from "react-redux";
import type { AppState } from "ee/reducers";
@ -20,6 +21,8 @@ import { Field, formValueSelector, reduxForm } from "redux-form";
import styled from "styled-components";
import { proficiencyOptions, useCaseOptions } from "./constants";
import RadioButtonGroup from "components/editorComponents/RadioButtonGroup";
import FormTextField from "components/utils/ReduxFormTextField";
import { useIsCloudBillingEnabled } from "hooks";
const ActionContainer = styled.div`
margin-top: ${(props) => props.theme.spaces[15]}px;
@ -64,13 +67,26 @@ const validate = (values: any) => {
function NonSuperUserProfilingQuestions(
props: InjectedFormProps & UserFormProps & NonSuperUserFormData,
) {
const isCloudBillingEnabled = useIsCloudBillingEnabled();
const onSubmit = (data: NonSuperUserFormData) => {
props.onGetStarted && props.onGetStarted(data.proficiency, data.useCase);
};
return (
<form onSubmit={props.handleSubmit(onSubmit)}>
<Space />
{isCloudBillingEnabled && (
<>
<Space />
<FormTextField
data-testid="t--user-full-name"
label={createMessage(WELCOME_FORM_FULL_NAME)}
name="fullName"
placeholder="Enter your full name"
/>
<Space />
</>
)}
<Field
component={RadioButtonGroup}
label={createMessage(WELCOME_FORM_NON_SUPER_USER_PROFICIENCY_LEVEL)}

View File

@ -0,0 +1,13 @@
import { createSelector } from "reselect";
import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors";
/**
* Checks if the cloud billing is enabled via the license_multi_org_enabled feature flag
*
* @returns boolean
*/
export const getIsCloudBillingFeatureFlagEnabled = createSelector(
selectFeatureFlags,
(featureFlags) => featureFlags.license_multi_org_enabled,
);

View File

@ -0,0 +1,5 @@
export function isLoginHostname(): boolean {
const hostname = window.location.hostname;
return hostname.split(".")[0] === "login";
}